@reviewflow/sdk 6.21.0 → 6.23.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/build/index-browser.es.js +182 -11
- package/build/index-node.mjs +182 -11
- package/package.json +15 -15
- package/src/index.ts +3 -0
- package/src/reviewflow/boardFacts.ts +184 -0
- package/src/utils/prStatus.ts +152 -0
- package/src/utils/relativeDate.ts +56 -0
|
@@ -1023,9 +1023,7 @@ const restComment = (comment) => ({
|
|
|
1023
1023
|
line: comment.line,
|
|
1024
1024
|
side: comment.side,
|
|
1025
1025
|
...comment.startLine === null || comment.startSide === null ? {} : {
|
|
1026
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1027
1026
|
start_line: comment.startLine,
|
|
1028
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1029
1027
|
start_side: comment.startSide
|
|
1030
1028
|
}
|
|
1031
1029
|
});
|
|
@@ -1091,7 +1089,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1091
1089
|
await transport.rest({
|
|
1092
1090
|
method: "POST",
|
|
1093
1091
|
path: `${pullPath}/requested_reviewers`,
|
|
1094
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1095
1092
|
body: { reviewers: edit.add, team_reviewers: edit.addTeams }
|
|
1096
1093
|
});
|
|
1097
1094
|
}
|
|
@@ -1099,7 +1096,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1099
1096
|
await transport.rest({
|
|
1100
1097
|
method: "DELETE",
|
|
1101
1098
|
path: `${pullPath}/requested_reviewers`,
|
|
1102
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1103
1099
|
body: { reviewers: edit.remove, team_reviewers: edit.removeTeams }
|
|
1104
1100
|
});
|
|
1105
1101
|
}
|
|
@@ -1147,13 +1143,10 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1147
1143
|
method: "PUT",
|
|
1148
1144
|
path: `${pullPath}/merge`,
|
|
1149
1145
|
body: {
|
|
1150
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1151
1146
|
merge_method: edit.method,
|
|
1152
1147
|
sha: edit.expectedHeadSha,
|
|
1153
1148
|
...edit.commitMessage === void 0 ? {} : {
|
|
1154
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1155
1149
|
commit_title: edit.commitMessage.title,
|
|
1156
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1157
1150
|
commit_message: edit.commitMessage.body
|
|
1158
1151
|
}
|
|
1159
1152
|
}
|
|
@@ -1164,7 +1157,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1164
1157
|
await transport.rest({
|
|
1165
1158
|
method: "PUT",
|
|
1166
1159
|
path: `${pullPath}/update-branch`,
|
|
1167
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1168
1160
|
body: { expected_head_sha: edit.expectedHeadSha }
|
|
1169
1161
|
});
|
|
1170
1162
|
return null;
|
|
@@ -1244,7 +1236,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1244
1236
|
method: "POST",
|
|
1245
1237
|
path: `${pullPath}/reviews`,
|
|
1246
1238
|
body: {
|
|
1247
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1248
1239
|
commit_id: edit.commitId,
|
|
1249
1240
|
...edit.body === "" ? {} : { body: edit.body },
|
|
1250
1241
|
event: reviewEventNames[edit.event],
|
|
@@ -1264,7 +1255,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1264
1255
|
await transport.rest({
|
|
1265
1256
|
method: "POST",
|
|
1266
1257
|
path: `${pullPath}/comments`,
|
|
1267
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1268
1258
|
body: { commit_id: edit.commitId, ...restComment(edit.comment) }
|
|
1269
1259
|
});
|
|
1270
1260
|
return null;
|
|
@@ -2317,6 +2307,75 @@ const createGitHubTransport = (send) => ({
|
|
|
2317
2307
|
}
|
|
2318
2308
|
});
|
|
2319
2309
|
|
|
2310
|
+
const prStatuses = [
|
|
2311
|
+
"approved",
|
|
2312
|
+
"changesRequested",
|
|
2313
|
+
"closed",
|
|
2314
|
+
"draft",
|
|
2315
|
+
"merged",
|
|
2316
|
+
"noReviewers",
|
|
2317
|
+
"waitingReview"
|
|
2318
|
+
];
|
|
2319
|
+
const reviewStatuses = {
|
|
2320
|
+
approved: "approved",
|
|
2321
|
+
changesRequested: "changesRequested",
|
|
2322
|
+
reviewRequired: "waitingReview"
|
|
2323
|
+
};
|
|
2324
|
+
const prStatus = ({
|
|
2325
|
+
state,
|
|
2326
|
+
reviewDecision
|
|
2327
|
+
}) => {
|
|
2328
|
+
if (state !== "open") return state;
|
|
2329
|
+
return reviewDecision === null ? "noReviewers" : reviewStatuses[reviewDecision];
|
|
2330
|
+
};
|
|
2331
|
+
const prStatusLabels = {
|
|
2332
|
+
approved: "approved",
|
|
2333
|
+
changesRequested: "changes requested",
|
|
2334
|
+
closed: "closed",
|
|
2335
|
+
draft: "draft",
|
|
2336
|
+
merged: "merged",
|
|
2337
|
+
noReviewers: "no reviewers",
|
|
2338
|
+
waitingReview: "waiting review"
|
|
2339
|
+
};
|
|
2340
|
+
const prHasConflicts = ({
|
|
2341
|
+
state,
|
|
2342
|
+
mergeable
|
|
2343
|
+
}) => (state === "open" || state === "draft") && mergeable === "conflicting";
|
|
2344
|
+
const prUrl = (ownerRepo, number) => `https://github.com/${ownerRepo}/pull/${String(number)}`;
|
|
2345
|
+
const prNumberPattern = /^#?(\d+)$/;
|
|
2346
|
+
const prUrlPattern = /^https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#]\S*)?$/i;
|
|
2347
|
+
const prNumberOf = (input, ownerRepo) => {
|
|
2348
|
+
const text = input.trim();
|
|
2349
|
+
const url = prUrlPattern.exec(text);
|
|
2350
|
+
const [, digits] = url === null ? prNumberPattern.exec(text) ?? [] : [];
|
|
2351
|
+
if (url !== null && url[1]?.toLowerCase() !== ownerRepo.toLowerCase()) {
|
|
2352
|
+
return `that pull request is not in ${ownerRepo}`;
|
|
2353
|
+
}
|
|
2354
|
+
const number = Number(url?.[2] ?? digits);
|
|
2355
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
2356
|
+
return "a pull request number or its GitHub link is expected";
|
|
2357
|
+
}
|
|
2358
|
+
return number;
|
|
2359
|
+
};
|
|
2360
|
+
const reviewflowReviewDecisions = {
|
|
2361
|
+
approved: "approved",
|
|
2362
|
+
changesRequested: "changesRequested",
|
|
2363
|
+
requested: "reviewRequired",
|
|
2364
|
+
none: null
|
|
2365
|
+
};
|
|
2366
|
+
const reviewflowPrStatus = ({
|
|
2367
|
+
state,
|
|
2368
|
+
reviewState
|
|
2369
|
+
}) => {
|
|
2370
|
+
if (state === null) return null;
|
|
2371
|
+
if (state !== "open") return state;
|
|
2372
|
+
if (reviewState === null) return null;
|
|
2373
|
+
return prStatus({
|
|
2374
|
+
state,
|
|
2375
|
+
reviewDecision: reviewflowReviewDecisions[reviewState]
|
|
2376
|
+
});
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2320
2379
|
const pullRequestParts = [
|
|
2321
2380
|
"detail",
|
|
2322
2381
|
"checks",
|
|
@@ -2401,6 +2460,32 @@ const pullRequestEditMovesBoard = (edit) => {
|
|
|
2401
2460
|
}
|
|
2402
2461
|
};
|
|
2403
2462
|
|
|
2463
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
2464
|
+
const weekdayReach = 6;
|
|
2465
|
+
const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
2466
|
+
const calendarDaysAgo = (date, now) => Math.round((startOfDay(now) - startOfDay(date)) / dayMs);
|
|
2467
|
+
const formatRelativeDate = (value, { now = /* @__PURE__ */ new Date(), locale } = {}) => {
|
|
2468
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
2469
|
+
if (Number.isNaN(date.getTime())) return "unknown date";
|
|
2470
|
+
const time = date.toLocaleTimeString(locale, {
|
|
2471
|
+
hour: "2-digit",
|
|
2472
|
+
minute: "2-digit"
|
|
2473
|
+
});
|
|
2474
|
+
const days = calendarDaysAgo(date, now);
|
|
2475
|
+
if (days === 0) return `today ${time}`;
|
|
2476
|
+
if (days === 1) return `yesterday ${time}`;
|
|
2477
|
+
if (days > 1 && days <= weekdayReach) {
|
|
2478
|
+
const weekday = date.toLocaleDateString("en", { weekday: "long" });
|
|
2479
|
+
return `last ${weekday} ${time}`;
|
|
2480
|
+
}
|
|
2481
|
+
const day = date.toLocaleDateString(locale, {
|
|
2482
|
+
day: "numeric",
|
|
2483
|
+
month: "short",
|
|
2484
|
+
...date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }
|
|
2485
|
+
});
|
|
2486
|
+
return `${day}, ${time}`;
|
|
2487
|
+
};
|
|
2488
|
+
|
|
2404
2489
|
const partFields = {
|
|
2405
2490
|
detail: (pr) => [
|
|
2406
2491
|
pr.title,
|
|
@@ -3074,6 +3159,92 @@ const applyStreamEvent = (snapshot, event) => {
|
|
|
3074
3159
|
};
|
|
3075
3160
|
};
|
|
3076
3161
|
|
|
3162
|
+
const maxFailedCheckNames = 9;
|
|
3163
|
+
const bucketsSilentOnPassedChecks = /* @__PURE__ */ new Set(["changes-requested", "drafts"]);
|
|
3164
|
+
const reRequestedBucket = "re-requested-reviews";
|
|
3165
|
+
const requestingAttentionGroup = "requesting-attention";
|
|
3166
|
+
const counted = (count, one, many) => `${String(count)} ${count === 1 ? one : many}`;
|
|
3167
|
+
const people = (logins, viewer) => logins.map((login) => login === viewer ? "you" : `@${login}`).join(", ");
|
|
3168
|
+
const failedChecksText = ({
|
|
3169
|
+
checksConclusion,
|
|
3170
|
+
failedChecks
|
|
3171
|
+
}) => {
|
|
3172
|
+
if (failedChecks.length === 0) {
|
|
3173
|
+
return checksConclusion === "failed" ? "checks failed" : null;
|
|
3174
|
+
}
|
|
3175
|
+
const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
|
|
3176
|
+
const remaining = failedChecks.length - maxFailedCheckNames;
|
|
3177
|
+
const more = remaining > 0 ? ` +${String(remaining)}` : "";
|
|
3178
|
+
return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
|
|
3179
|
+
};
|
|
3180
|
+
const reviewflowPrFailure = (pr) => {
|
|
3181
|
+
const parts = [
|
|
3182
|
+
failedChecksText(pr),
|
|
3183
|
+
pr.lintFailed ? "pr lint failed" : null,
|
|
3184
|
+
pr.mergeable === "conflicting" ? "conflicts" : null
|
|
3185
|
+
].filter((part) => part !== null);
|
|
3186
|
+
return parts.length === 0 ? null : parts.join(" \xB7 ");
|
|
3187
|
+
};
|
|
3188
|
+
const reviewflowPrChangesRequested = ({ changesRequestedBy }, viewer) => changesRequestedBy.length === 0 ? null : `changes requested by ${people(changesRequestedBy, viewer)}`;
|
|
3189
|
+
const checksProgressText = ({
|
|
3190
|
+
checksConclusion,
|
|
3191
|
+
checksRunning
|
|
3192
|
+
}, bucketKey) => {
|
|
3193
|
+
if (checksRunning > 0) {
|
|
3194
|
+
return `${counted(checksRunning, "check", "checks")} running`;
|
|
3195
|
+
}
|
|
3196
|
+
return checksConclusion === "passed" && !bucketsSilentOnPassedChecks.has(bucketKey) ? "checks passed" : null;
|
|
3197
|
+
};
|
|
3198
|
+
const awaitedText = ({
|
|
3199
|
+
requestedReviewers,
|
|
3200
|
+
reRequestedReviewers,
|
|
3201
|
+
requestedTeams
|
|
3202
|
+
}, { bucketKey, groupKey, viewer }) => {
|
|
3203
|
+
const reviewers = [
|
|
3204
|
+
...requestedReviewers,
|
|
3205
|
+
...bucketKey === reRequestedBucket ? [] : reRequestedReviewers
|
|
3206
|
+
];
|
|
3207
|
+
if (reviewers.length === 0 && requestedTeams.length === 0) return null;
|
|
3208
|
+
const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
|
|
3209
|
+
const awaited = [
|
|
3210
|
+
...reviewers.length === 0 ? [] : [people(reviewers, viewer)],
|
|
3211
|
+
...requestedTeams.map((team) => `#${team}`)
|
|
3212
|
+
];
|
|
3213
|
+
return `${verb} ${awaited.join(", ")}`;
|
|
3214
|
+
};
|
|
3215
|
+
const reviewflowPrProgress = (pr, context) => {
|
|
3216
|
+
const parts = [
|
|
3217
|
+
checksProgressText(pr, context.bucketKey),
|
|
3218
|
+
pr.approvedBy.length === 0 ? null : `approved by ${people(pr.approvedBy, context.viewer)}`,
|
|
3219
|
+
pr.unresolvedThreads ? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads") : null,
|
|
3220
|
+
awaitedText(pr, context)
|
|
3221
|
+
].filter((part) => part !== null);
|
|
3222
|
+
return parts.length === 0 ? null : parts.join(" \xB7 ");
|
|
3223
|
+
};
|
|
3224
|
+
const reviewflowPrOwners = ({ author, assignees }, viewer) => {
|
|
3225
|
+
const named = [...author === null ? [] : [author], ...assignees];
|
|
3226
|
+
if (named.length === 0) return null;
|
|
3227
|
+
if (viewer !== null && named.every((login) => login === viewer)) return null;
|
|
3228
|
+
const byline = author === null ? null : `by ${people([author], viewer)}`;
|
|
3229
|
+
if (assignees.length === 0) return byline;
|
|
3230
|
+
const assignment = `assigned to ${people(assignees, viewer)}`;
|
|
3231
|
+
if (author === null || !assignees.includes(author)) {
|
|
3232
|
+
return byline === null ? assignment : `${byline} \xB7 ${assignment}`;
|
|
3233
|
+
}
|
|
3234
|
+
return assignees.length === 1 ? byline : assignment;
|
|
3235
|
+
};
|
|
3236
|
+
const reviewflowPrFlowDate = ({ openedAt, approvedAt }, options) => {
|
|
3237
|
+
if (approvedAt !== null) {
|
|
3238
|
+
return `approved ${formatRelativeDate(approvedAt, options)}`;
|
|
3239
|
+
}
|
|
3240
|
+
return openedAt === null ? null : `opened ${formatRelativeDate(openedAt, options)}`;
|
|
3241
|
+
};
|
|
3242
|
+
const formatPrChanges = ({
|
|
3243
|
+
changedFiles,
|
|
3244
|
+
additions,
|
|
3245
|
+
deletions
|
|
3246
|
+
}) => `${counted(changedFiles, "file", "files")} +${String(additions)} \u2212${String(deletions)}`;
|
|
3247
|
+
|
|
3077
3248
|
const lingerMs = 5 * 6e4;
|
|
3078
3249
|
const boardReadDelayMs = 3e3;
|
|
3079
3250
|
const reconnectDelayMs = 1e3;
|
|
@@ -3334,5 +3505,5 @@ const insertCheckoutEntry = (source, ownerRepo) => {
|
|
|
3334
3505
|
return { source: text, cursor: value };
|
|
3335
3506
|
};
|
|
3336
3507
|
|
|
3337
|
-
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
|
|
3508
|
+
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, formatPrChanges, formatRelativeDate, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxFailedCheckNames, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, prHasConflicts, prNumberOf, prStatus, prStatusLabels, prStatuses, prUrl, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, reviewflowPrChangesRequested, reviewflowPrFailure, reviewflowPrFlowDate, reviewflowPrOwners, reviewflowPrProgress, reviewflowPrStatus, reviewflowReviewDecisions, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
|
|
3338
3509
|
//# sourceMappingURL=index-browser.es.js.map
|
package/build/index-node.mjs
CHANGED
|
@@ -1023,9 +1023,7 @@ const restComment = (comment) => ({
|
|
|
1023
1023
|
line: comment.line,
|
|
1024
1024
|
side: comment.side,
|
|
1025
1025
|
...comment.startLine === null || comment.startSide === null ? {} : {
|
|
1026
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1027
1026
|
start_line: comment.startLine,
|
|
1028
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1029
1027
|
start_side: comment.startSide
|
|
1030
1028
|
}
|
|
1031
1029
|
});
|
|
@@ -1091,7 +1089,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1091
1089
|
await transport.rest({
|
|
1092
1090
|
method: "POST",
|
|
1093
1091
|
path: `${pullPath}/requested_reviewers`,
|
|
1094
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1095
1092
|
body: { reviewers: edit.add, team_reviewers: edit.addTeams }
|
|
1096
1093
|
});
|
|
1097
1094
|
}
|
|
@@ -1099,7 +1096,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1099
1096
|
await transport.rest({
|
|
1100
1097
|
method: "DELETE",
|
|
1101
1098
|
path: `${pullPath}/requested_reviewers`,
|
|
1102
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1103
1099
|
body: { reviewers: edit.remove, team_reviewers: edit.removeTeams }
|
|
1104
1100
|
});
|
|
1105
1101
|
}
|
|
@@ -1147,13 +1143,10 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1147
1143
|
method: "PUT",
|
|
1148
1144
|
path: `${pullPath}/merge`,
|
|
1149
1145
|
body: {
|
|
1150
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1151
1146
|
merge_method: edit.method,
|
|
1152
1147
|
sha: edit.expectedHeadSha,
|
|
1153
1148
|
...edit.commitMessage === void 0 ? {} : {
|
|
1154
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1155
1149
|
commit_title: edit.commitMessage.title,
|
|
1156
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1157
1150
|
commit_message: edit.commitMessage.body
|
|
1158
1151
|
}
|
|
1159
1152
|
}
|
|
@@ -1164,7 +1157,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1164
1157
|
await transport.rest({
|
|
1165
1158
|
method: "PUT",
|
|
1166
1159
|
path: `${pullPath}/update-branch`,
|
|
1167
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1168
1160
|
body: { expected_head_sha: edit.expectedHeadSha }
|
|
1169
1161
|
});
|
|
1170
1162
|
return null;
|
|
@@ -1244,7 +1236,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1244
1236
|
method: "POST",
|
|
1245
1237
|
path: `${pullPath}/reviews`,
|
|
1246
1238
|
body: {
|
|
1247
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1248
1239
|
commit_id: edit.commitId,
|
|
1249
1240
|
...edit.body === "" ? {} : { body: edit.body },
|
|
1250
1241
|
event: reviewEventNames[edit.event],
|
|
@@ -1264,7 +1255,6 @@ const editPullRequest = async (transport, { ownerRepo, number, edit }, options =
|
|
|
1264
1255
|
await transport.rest({
|
|
1265
1256
|
method: "POST",
|
|
1266
1257
|
path: `${pullPath}/comments`,
|
|
1267
|
-
// eslint-disable-next-line camelcase -- GitHub's field name
|
|
1268
1258
|
body: { commit_id: edit.commitId, ...restComment(edit.comment) }
|
|
1269
1259
|
});
|
|
1270
1260
|
return null;
|
|
@@ -2317,6 +2307,75 @@ const createGitHubTransport = (send) => ({
|
|
|
2317
2307
|
}
|
|
2318
2308
|
});
|
|
2319
2309
|
|
|
2310
|
+
const prStatuses = [
|
|
2311
|
+
"approved",
|
|
2312
|
+
"changesRequested",
|
|
2313
|
+
"closed",
|
|
2314
|
+
"draft",
|
|
2315
|
+
"merged",
|
|
2316
|
+
"noReviewers",
|
|
2317
|
+
"waitingReview"
|
|
2318
|
+
];
|
|
2319
|
+
const reviewStatuses = {
|
|
2320
|
+
approved: "approved",
|
|
2321
|
+
changesRequested: "changesRequested",
|
|
2322
|
+
reviewRequired: "waitingReview"
|
|
2323
|
+
};
|
|
2324
|
+
const prStatus = ({
|
|
2325
|
+
state,
|
|
2326
|
+
reviewDecision
|
|
2327
|
+
}) => {
|
|
2328
|
+
if (state !== "open") return state;
|
|
2329
|
+
return reviewDecision === null ? "noReviewers" : reviewStatuses[reviewDecision];
|
|
2330
|
+
};
|
|
2331
|
+
const prStatusLabels = {
|
|
2332
|
+
approved: "approved",
|
|
2333
|
+
changesRequested: "changes requested",
|
|
2334
|
+
closed: "closed",
|
|
2335
|
+
draft: "draft",
|
|
2336
|
+
merged: "merged",
|
|
2337
|
+
noReviewers: "no reviewers",
|
|
2338
|
+
waitingReview: "waiting review"
|
|
2339
|
+
};
|
|
2340
|
+
const prHasConflicts = ({
|
|
2341
|
+
state,
|
|
2342
|
+
mergeable
|
|
2343
|
+
}) => (state === "open" || state === "draft") && mergeable === "conflicting";
|
|
2344
|
+
const prUrl = (ownerRepo, number) => `https://github.com/${ownerRepo}/pull/${String(number)}`;
|
|
2345
|
+
const prNumberPattern = /^#?(\d+)$/;
|
|
2346
|
+
const prUrlPattern = /^https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#]\S*)?$/i;
|
|
2347
|
+
const prNumberOf = (input, ownerRepo) => {
|
|
2348
|
+
const text = input.trim();
|
|
2349
|
+
const url = prUrlPattern.exec(text);
|
|
2350
|
+
const [, digits] = url === null ? prNumberPattern.exec(text) ?? [] : [];
|
|
2351
|
+
if (url !== null && url[1]?.toLowerCase() !== ownerRepo.toLowerCase()) {
|
|
2352
|
+
return `that pull request is not in ${ownerRepo}`;
|
|
2353
|
+
}
|
|
2354
|
+
const number = Number(url?.[2] ?? digits);
|
|
2355
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
2356
|
+
return "a pull request number or its GitHub link is expected";
|
|
2357
|
+
}
|
|
2358
|
+
return number;
|
|
2359
|
+
};
|
|
2360
|
+
const reviewflowReviewDecisions = {
|
|
2361
|
+
approved: "approved",
|
|
2362
|
+
changesRequested: "changesRequested",
|
|
2363
|
+
requested: "reviewRequired",
|
|
2364
|
+
none: null
|
|
2365
|
+
};
|
|
2366
|
+
const reviewflowPrStatus = ({
|
|
2367
|
+
state,
|
|
2368
|
+
reviewState
|
|
2369
|
+
}) => {
|
|
2370
|
+
if (state === null) return null;
|
|
2371
|
+
if (state !== "open") return state;
|
|
2372
|
+
if (reviewState === null) return null;
|
|
2373
|
+
return prStatus({
|
|
2374
|
+
state,
|
|
2375
|
+
reviewDecision: reviewflowReviewDecisions[reviewState]
|
|
2376
|
+
});
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2320
2379
|
const pullRequestParts = [
|
|
2321
2380
|
"detail",
|
|
2322
2381
|
"checks",
|
|
@@ -2401,6 +2460,32 @@ const pullRequestEditMovesBoard = (edit) => {
|
|
|
2401
2460
|
}
|
|
2402
2461
|
};
|
|
2403
2462
|
|
|
2463
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
2464
|
+
const weekdayReach = 6;
|
|
2465
|
+
const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
2466
|
+
const calendarDaysAgo = (date, now) => Math.round((startOfDay(now) - startOfDay(date)) / dayMs);
|
|
2467
|
+
const formatRelativeDate = (value, { now = /* @__PURE__ */ new Date(), locale } = {}) => {
|
|
2468
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
2469
|
+
if (Number.isNaN(date.getTime())) return "unknown date";
|
|
2470
|
+
const time = date.toLocaleTimeString(locale, {
|
|
2471
|
+
hour: "2-digit",
|
|
2472
|
+
minute: "2-digit"
|
|
2473
|
+
});
|
|
2474
|
+
const days = calendarDaysAgo(date, now);
|
|
2475
|
+
if (days === 0) return `today ${time}`;
|
|
2476
|
+
if (days === 1) return `yesterday ${time}`;
|
|
2477
|
+
if (days > 1 && days <= weekdayReach) {
|
|
2478
|
+
const weekday = date.toLocaleDateString("en", { weekday: "long" });
|
|
2479
|
+
return `last ${weekday} ${time}`;
|
|
2480
|
+
}
|
|
2481
|
+
const day = date.toLocaleDateString(locale, {
|
|
2482
|
+
day: "numeric",
|
|
2483
|
+
month: "short",
|
|
2484
|
+
...date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }
|
|
2485
|
+
});
|
|
2486
|
+
return `${day}, ${time}`;
|
|
2487
|
+
};
|
|
2488
|
+
|
|
2404
2489
|
const partFields = {
|
|
2405
2490
|
detail: (pr) => [
|
|
2406
2491
|
pr.title,
|
|
@@ -3074,6 +3159,92 @@ const applyStreamEvent = (snapshot, event) => {
|
|
|
3074
3159
|
};
|
|
3075
3160
|
};
|
|
3076
3161
|
|
|
3162
|
+
const maxFailedCheckNames = 9;
|
|
3163
|
+
const bucketsSilentOnPassedChecks = /* @__PURE__ */ new Set(["changes-requested", "drafts"]);
|
|
3164
|
+
const reRequestedBucket = "re-requested-reviews";
|
|
3165
|
+
const requestingAttentionGroup = "requesting-attention";
|
|
3166
|
+
const counted = (count, one, many) => `${String(count)} ${count === 1 ? one : many}`;
|
|
3167
|
+
const people = (logins, viewer) => logins.map((login) => login === viewer ? "you" : `@${login}`).join(", ");
|
|
3168
|
+
const failedChecksText = ({
|
|
3169
|
+
checksConclusion,
|
|
3170
|
+
failedChecks
|
|
3171
|
+
}) => {
|
|
3172
|
+
if (failedChecks.length === 0) {
|
|
3173
|
+
return checksConclusion === "failed" ? "checks failed" : null;
|
|
3174
|
+
}
|
|
3175
|
+
const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
|
|
3176
|
+
const remaining = failedChecks.length - maxFailedCheckNames;
|
|
3177
|
+
const more = remaining > 0 ? ` +${String(remaining)}` : "";
|
|
3178
|
+
return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
|
|
3179
|
+
};
|
|
3180
|
+
const reviewflowPrFailure = (pr) => {
|
|
3181
|
+
const parts = [
|
|
3182
|
+
failedChecksText(pr),
|
|
3183
|
+
pr.lintFailed ? "pr lint failed" : null,
|
|
3184
|
+
pr.mergeable === "conflicting" ? "conflicts" : null
|
|
3185
|
+
].filter((part) => part !== null);
|
|
3186
|
+
return parts.length === 0 ? null : parts.join(" \xB7 ");
|
|
3187
|
+
};
|
|
3188
|
+
const reviewflowPrChangesRequested = ({ changesRequestedBy }, viewer) => changesRequestedBy.length === 0 ? null : `changes requested by ${people(changesRequestedBy, viewer)}`;
|
|
3189
|
+
const checksProgressText = ({
|
|
3190
|
+
checksConclusion,
|
|
3191
|
+
checksRunning
|
|
3192
|
+
}, bucketKey) => {
|
|
3193
|
+
if (checksRunning > 0) {
|
|
3194
|
+
return `${counted(checksRunning, "check", "checks")} running`;
|
|
3195
|
+
}
|
|
3196
|
+
return checksConclusion === "passed" && !bucketsSilentOnPassedChecks.has(bucketKey) ? "checks passed" : null;
|
|
3197
|
+
};
|
|
3198
|
+
const awaitedText = ({
|
|
3199
|
+
requestedReviewers,
|
|
3200
|
+
reRequestedReviewers,
|
|
3201
|
+
requestedTeams
|
|
3202
|
+
}, { bucketKey, groupKey, viewer }) => {
|
|
3203
|
+
const reviewers = [
|
|
3204
|
+
...requestedReviewers,
|
|
3205
|
+
...bucketKey === reRequestedBucket ? [] : reRequestedReviewers
|
|
3206
|
+
];
|
|
3207
|
+
if (reviewers.length === 0 && requestedTeams.length === 0) return null;
|
|
3208
|
+
const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
|
|
3209
|
+
const awaited = [
|
|
3210
|
+
...reviewers.length === 0 ? [] : [people(reviewers, viewer)],
|
|
3211
|
+
...requestedTeams.map((team) => `#${team}`)
|
|
3212
|
+
];
|
|
3213
|
+
return `${verb} ${awaited.join(", ")}`;
|
|
3214
|
+
};
|
|
3215
|
+
const reviewflowPrProgress = (pr, context) => {
|
|
3216
|
+
const parts = [
|
|
3217
|
+
checksProgressText(pr, context.bucketKey),
|
|
3218
|
+
pr.approvedBy.length === 0 ? null : `approved by ${people(pr.approvedBy, context.viewer)}`,
|
|
3219
|
+
pr.unresolvedThreads ? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads") : null,
|
|
3220
|
+
awaitedText(pr, context)
|
|
3221
|
+
].filter((part) => part !== null);
|
|
3222
|
+
return parts.length === 0 ? null : parts.join(" \xB7 ");
|
|
3223
|
+
};
|
|
3224
|
+
const reviewflowPrOwners = ({ author, assignees }, viewer) => {
|
|
3225
|
+
const named = [...author === null ? [] : [author], ...assignees];
|
|
3226
|
+
if (named.length === 0) return null;
|
|
3227
|
+
if (viewer !== null && named.every((login) => login === viewer)) return null;
|
|
3228
|
+
const byline = author === null ? null : `by ${people([author], viewer)}`;
|
|
3229
|
+
if (assignees.length === 0) return byline;
|
|
3230
|
+
const assignment = `assigned to ${people(assignees, viewer)}`;
|
|
3231
|
+
if (author === null || !assignees.includes(author)) {
|
|
3232
|
+
return byline === null ? assignment : `${byline} \xB7 ${assignment}`;
|
|
3233
|
+
}
|
|
3234
|
+
return assignees.length === 1 ? byline : assignment;
|
|
3235
|
+
};
|
|
3236
|
+
const reviewflowPrFlowDate = ({ openedAt, approvedAt }, options) => {
|
|
3237
|
+
if (approvedAt !== null) {
|
|
3238
|
+
return `approved ${formatRelativeDate(approvedAt, options)}`;
|
|
3239
|
+
}
|
|
3240
|
+
return openedAt === null ? null : `opened ${formatRelativeDate(openedAt, options)}`;
|
|
3241
|
+
};
|
|
3242
|
+
const formatPrChanges = ({
|
|
3243
|
+
changedFiles,
|
|
3244
|
+
additions,
|
|
3245
|
+
deletions
|
|
3246
|
+
}) => `${counted(changedFiles, "file", "files")} +${String(additions)} \u2212${String(deletions)}`;
|
|
3247
|
+
|
|
3077
3248
|
const lingerMs = 5 * 6e4;
|
|
3078
3249
|
const boardReadDelayMs = 3e3;
|
|
3079
3250
|
const reconnectDelayMs = 1e3;
|
|
@@ -3334,5 +3505,5 @@ const insertCheckoutEntry = (source, ownerRepo) => {
|
|
|
3334
3505
|
return { source: text, cursor: value };
|
|
3335
3506
|
};
|
|
3336
3507
|
|
|
3337
|
-
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
|
|
3508
|
+
export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, formatPrChanges, formatRelativeDate, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxFailedCheckNames, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, prHasConflicts, prNumberOf, prStatus, prStatusLabels, prStatuses, prUrl, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, reviewflowPrChangesRequested, reviewflowPrFailure, reviewflowPrFlowDate, reviewflowPrOwners, reviewflowPrProgress, reviewflowPrStatus, reviewflowReviewDecisions, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
|
|
3338
3509
|
//# sourceMappingURL=index-node.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reviewflow/sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.23.0",
|
|
4
4
|
"description": "pull request model shared by the reviewflow app server and its ui: diff and api types, pure utils, the GitHub layer behind an injected transport, and (node only) reading a diff from a local checkout (@reviewflow/sdk/git) and editing a text file in place (@reviewflow/sdk/files)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -61,19 +61,6 @@
|
|
|
61
61
|
"publishConfig": {
|
|
62
62
|
"access": "public"
|
|
63
63
|
},
|
|
64
|
-
"scripts": {
|
|
65
|
-
"build": "pnpm run clean:build && rollup --config rollup.config.mjs",
|
|
66
|
-
"clean": "pnpm run clean:build",
|
|
67
|
-
"clean:build": "pob-esbuild-clean-out build",
|
|
68
|
-
"lint": "pnpm run lint:oxlint && pnpm run lint:eslint",
|
|
69
|
-
"lint:eslint": "pnpm --dir '../..' exec eslint --quiet 'packages/sdk'",
|
|
70
|
-
"lint:oxlint": "pnpm --dir '../..' exec oxlint --quiet 'packages/sdk'",
|
|
71
|
-
"start": "pnpm run clean:build && rollup --config rollup.config.mjs --watch",
|
|
72
|
-
"test": "pnpm run --dir '../..' test -- 'packages/sdk'",
|
|
73
|
-
"tsc": "tsrs",
|
|
74
|
-
"tsc:go": "tsc",
|
|
75
|
-
"watch": "pnpm run clean:build && rollup --config rollup.config.mjs --watch"
|
|
76
|
-
},
|
|
77
64
|
"dependencies": {
|
|
78
65
|
"oxc-parser": "0.127.0"
|
|
79
66
|
},
|
|
@@ -134,5 +121,18 @@
|
|
|
134
121
|
}
|
|
135
122
|
],
|
|
136
123
|
"typescript": true
|
|
124
|
+
},
|
|
125
|
+
"scripts": {
|
|
126
|
+
"build": "pnpm run clean:build && rollup --config rollup.config.mjs",
|
|
127
|
+
"clean": "pnpm run clean:build",
|
|
128
|
+
"clean:build": "pob-esbuild-clean-out build",
|
|
129
|
+
"lint": "pnpm run lint:oxlint && pnpm run lint:eslint",
|
|
130
|
+
"lint:eslint": "pnpm --dir '../..' exec eslint --quiet 'packages/sdk'",
|
|
131
|
+
"lint:oxlint": "pnpm --dir '../..' exec oxlint --quiet 'packages/sdk'",
|
|
132
|
+
"start": "pnpm run clean:build && rollup --config rollup.config.mjs --watch",
|
|
133
|
+
"test": "pnpm run --dir '../..' test -- 'packages/sdk'",
|
|
134
|
+
"tsc": "tsrs",
|
|
135
|
+
"tsc:go": "tsc",
|
|
136
|
+
"watch": "pnpm run clean:build && rollup --config rollup.config.mjs --watch"
|
|
137
137
|
}
|
|
138
|
-
}
|
|
138
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,15 +20,18 @@ export type * from "./api/pullRequest.ts";
|
|
|
20
20
|
export type * from "./types/entities.ts";
|
|
21
21
|
export type * from "./types/ids.ts";
|
|
22
22
|
export type * from "./types/reviewflow.ts";
|
|
23
|
+
export * from "./utils/prStatus.ts";
|
|
23
24
|
export * from "./utils/pullRequestEdit.ts";
|
|
24
25
|
export * from "./utils/pullRequestPart.ts";
|
|
25
26
|
export * from "./utils/pullRequestReaction.ts";
|
|
26
27
|
export * from "./utils/pullRequestSuggestion.ts";
|
|
28
|
+
export * from "./utils/relativeDate.ts";
|
|
27
29
|
export * from "./reviewflow/changedParts.ts";
|
|
28
30
|
export * from "./reviewflow/checksFromReviewflow.ts";
|
|
29
31
|
export * from "./reviewflow/client.ts";
|
|
30
32
|
export * from "./reviewflow/stream.ts";
|
|
31
33
|
export * from "./reviewflow/board.ts";
|
|
34
|
+
export * from "./reviewflow/boardFacts.ts";
|
|
32
35
|
export * from "./reviewflow/follow.ts";
|
|
33
36
|
export type * from "./reviewflow/status.ts";
|
|
34
37
|
export { insertCheckoutEntry } from "./config/insertCheckoutEntry.ts";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ReviewflowPrChanges } from "../types/reviewflow.ts";
|
|
2
|
+
import type { FormatRelativeDateOptions } from "../utils/relativeDate.ts";
|
|
3
|
+
import { formatRelativeDate } from "../utils/relativeDate.ts";
|
|
4
|
+
import type { ReviewflowBoardPr } from "./board.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What reviewflow's own board row says about a pull request, in its wording so
|
|
8
|
+
* the two boards read the same. The status line comes in three parts by how
|
|
9
|
+
* loudly each is shown: what blocks the merge, changes requested, the rest.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const maxFailedCheckNames = 9;
|
|
13
|
+
|
|
14
|
+
/** Where a row is shown, which decides what it leaves to its bucket's title. */
|
|
15
|
+
export interface ReviewflowRowContext {
|
|
16
|
+
bucketKey: string;
|
|
17
|
+
groupKey: string;
|
|
18
|
+
/** the token's login, named "you" */
|
|
19
|
+
viewer: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** the buckets whose title already says whether the checks pass */
|
|
23
|
+
const bucketsSilentOnPassedChecks = new Set(["changes-requested", "drafts"]);
|
|
24
|
+
/** the bucket whose title already says every reviewer was asked again */
|
|
25
|
+
const reRequestedBucket = "re-requested-reviews";
|
|
26
|
+
/** someone else's pull request requests a review, the operator's awaits one */
|
|
27
|
+
const requestingAttentionGroup = "requesting-attention";
|
|
28
|
+
|
|
29
|
+
const counted = (count: number, one: string, many: string): string =>
|
|
30
|
+
`${String(count)} ${count === 1 ? one : many}`;
|
|
31
|
+
|
|
32
|
+
const people = (logins: string[], viewer: string | null): string =>
|
|
33
|
+
logins.map((login) => (login === viewer ? "you" : `@${login}`)).join(", ");
|
|
34
|
+
|
|
35
|
+
const failedChecksText = ({
|
|
36
|
+
checksConclusion,
|
|
37
|
+
failedChecks,
|
|
38
|
+
}: Pick<ReviewflowBoardPr, "checksConclusion" | "failedChecks">):
|
|
39
|
+
| string
|
|
40
|
+
| null => {
|
|
41
|
+
if (failedChecks.length === 0) {
|
|
42
|
+
return checksConclusion === "failed" ? "checks failed" : null;
|
|
43
|
+
}
|
|
44
|
+
const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
|
|
45
|
+
const remaining = failedChecks.length - maxFailedCheckNames;
|
|
46
|
+
const more = remaining > 0 ? ` +${String(remaining)}` : "";
|
|
47
|
+
return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Failing checks (after reviewflow's allowed-to-fail rules, by name), a failed
|
|
52
|
+
* pr lint and a merge conflict; null when nothing blocks.
|
|
53
|
+
*/
|
|
54
|
+
export const reviewflowPrFailure = (
|
|
55
|
+
pr: Pick<
|
|
56
|
+
ReviewflowBoardPr,
|
|
57
|
+
"checksConclusion" | "failedChecks" | "lintFailed" | "mergeable"
|
|
58
|
+
>,
|
|
59
|
+
): string | null => {
|
|
60
|
+
const parts = [
|
|
61
|
+
failedChecksText(pr),
|
|
62
|
+
pr.lintFailed ? "pr lint failed" : null,
|
|
63
|
+
pr.mergeable === "conflicting" ? "conflicts" : null,
|
|
64
|
+
].filter((part) => part !== null);
|
|
65
|
+
return parts.length === 0 ? null : parts.join(" · ");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const reviewflowPrChangesRequested = (
|
|
69
|
+
{ changesRequestedBy }: Pick<ReviewflowBoardPr, "changesRequestedBy">,
|
|
70
|
+
viewer: string | null,
|
|
71
|
+
): string | null =>
|
|
72
|
+
changesRequestedBy.length === 0
|
|
73
|
+
? null
|
|
74
|
+
: `changes requested by ${people(changesRequestedBy, viewer)}`;
|
|
75
|
+
|
|
76
|
+
const checksProgressText = (
|
|
77
|
+
{
|
|
78
|
+
checksConclusion,
|
|
79
|
+
checksRunning,
|
|
80
|
+
}: Pick<ReviewflowBoardPr, "checksConclusion" | "checksRunning">,
|
|
81
|
+
bucketKey: string,
|
|
82
|
+
): string | null => {
|
|
83
|
+
if (checksRunning > 0) {
|
|
84
|
+
return `${counted(checksRunning, "check", "checks")} running`;
|
|
85
|
+
}
|
|
86
|
+
return checksConclusion === "passed" &&
|
|
87
|
+
!bucketsSilentOnPassedChecks.has(bucketKey)
|
|
88
|
+
? "checks passed"
|
|
89
|
+
: null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const awaitedText = (
|
|
93
|
+
{
|
|
94
|
+
requestedReviewers,
|
|
95
|
+
reRequestedReviewers,
|
|
96
|
+
requestedTeams,
|
|
97
|
+
}: Pick<
|
|
98
|
+
ReviewflowBoardPr,
|
|
99
|
+
"requestedReviewers" | "requestedTeams" | "reRequestedReviewers"
|
|
100
|
+
>,
|
|
101
|
+
{ bucketKey, groupKey, viewer }: ReviewflowRowContext,
|
|
102
|
+
): string | null => {
|
|
103
|
+
const reviewers = [
|
|
104
|
+
...requestedReviewers,
|
|
105
|
+
...(bucketKey === reRequestedBucket ? [] : reRequestedReviewers),
|
|
106
|
+
];
|
|
107
|
+
if (reviewers.length === 0 && requestedTeams.length === 0) return null;
|
|
108
|
+
const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
|
|
109
|
+
const awaited = [
|
|
110
|
+
...(reviewers.length === 0 ? [] : [people(reviewers, viewer)]),
|
|
111
|
+
...requestedTeams.map((team) => `#${team}`),
|
|
112
|
+
];
|
|
113
|
+
return `${verb} ${awaited.join(", ")}`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** running or passed checks, approvals, open threads, awaited reviewers */
|
|
117
|
+
export const reviewflowPrProgress = (
|
|
118
|
+
pr: Pick<
|
|
119
|
+
ReviewflowBoardPr,
|
|
120
|
+
| "approvedBy"
|
|
121
|
+
| "checksConclusion"
|
|
122
|
+
| "checksRunning"
|
|
123
|
+
| "requestedReviewers"
|
|
124
|
+
| "requestedTeams"
|
|
125
|
+
| "reRequestedReviewers"
|
|
126
|
+
| "unresolvedThreads"
|
|
127
|
+
>,
|
|
128
|
+
context: ReviewflowRowContext,
|
|
129
|
+
): string | null => {
|
|
130
|
+
const parts = [
|
|
131
|
+
checksProgressText(pr, context.bucketKey),
|
|
132
|
+
pr.approvedBy.length === 0
|
|
133
|
+
? null
|
|
134
|
+
: `approved by ${people(pr.approvedBy, context.viewer)}`,
|
|
135
|
+
pr.unresolvedThreads
|
|
136
|
+
? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads")
|
|
137
|
+
: null,
|
|
138
|
+
awaitedText(pr, context),
|
|
139
|
+
].filter((part) => part !== null);
|
|
140
|
+
return parts.length === 0 ? null : parts.join(" · ");
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* "by @author", "assigned to @a", or both. An author who is their own only
|
|
145
|
+
* assignee is a byline, not an assignment; a pull request whose only person is
|
|
146
|
+
* the viewer names nobody.
|
|
147
|
+
*/
|
|
148
|
+
export const reviewflowPrOwners = (
|
|
149
|
+
{ author, assignees }: Pick<ReviewflowBoardPr, "assignees" | "author">,
|
|
150
|
+
viewer: string | null,
|
|
151
|
+
): string | null => {
|
|
152
|
+
const named = [...(author === null ? [] : [author]), ...assignees];
|
|
153
|
+
if (named.length === 0) return null;
|
|
154
|
+
if (viewer !== null && named.every((login) => login === viewer)) return null;
|
|
155
|
+
|
|
156
|
+
const byline = author === null ? null : `by ${people([author], viewer)}`;
|
|
157
|
+
if (assignees.length === 0) return byline;
|
|
158
|
+
const assignment = `assigned to ${people(assignees, viewer)}`;
|
|
159
|
+
|
|
160
|
+
if (author === null || !assignees.includes(author)) {
|
|
161
|
+
return byline === null ? assignment : `${byline} · ${assignment}`;
|
|
162
|
+
}
|
|
163
|
+
// naming every assignee already names the author among them
|
|
164
|
+
return assignees.length === 1 ? byline : assignment;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const reviewflowPrFlowDate = (
|
|
168
|
+
{ openedAt, approvedAt }: Pick<ReviewflowBoardPr, "approvedAt" | "openedAt">,
|
|
169
|
+
options?: FormatRelativeDateOptions,
|
|
170
|
+
): string | null => {
|
|
171
|
+
if (approvedAt !== null) {
|
|
172
|
+
return `approved ${formatRelativeDate(approvedAt, options)}`;
|
|
173
|
+
}
|
|
174
|
+
return openedAt === null
|
|
175
|
+
? null
|
|
176
|
+
: `opened ${formatRelativeDate(openedAt, options)}`;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const formatPrChanges = ({
|
|
180
|
+
changedFiles,
|
|
181
|
+
additions,
|
|
182
|
+
deletions,
|
|
183
|
+
}: ReviewflowPrChanges): string =>
|
|
184
|
+
`${counted(changedFiles, "file", "files")} +${String(additions)} −${String(deletions)}`;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { ReviewflowBoardPr } from "../reviewflow/board.ts";
|
|
2
|
+
import type {
|
|
3
|
+
PrMergeable,
|
|
4
|
+
PrReviewDecision,
|
|
5
|
+
PrState,
|
|
6
|
+
} from "../types/entities.ts";
|
|
7
|
+
import type { ReviewflowReviewState } from "../types/reviewflow.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What the operator is told about a pull request, in one word or two.
|
|
11
|
+
*
|
|
12
|
+
* `pr.state` and `pr.reviewDecision` are two columns because GitHub reports two
|
|
13
|
+
* fields, but they are never two answers: an open PR is only ever interesting
|
|
14
|
+
* for where its review stands, and "open" on its own says nothing the branch did
|
|
15
|
+
* not already say. So the two collapse into one status, and every surface
|
|
16
|
+
* reads it from here rather than
|
|
17
|
+
* formatting the columns itself. They had drifted into three different renderings
|
|
18
|
+
* of the same row.
|
|
19
|
+
*
|
|
20
|
+
* `noReviewers` and `waitingReview` are a real distinction, not a null check:
|
|
21
|
+
* GitHub leaves `reviewDecision` null when nothing is required and nobody was
|
|
22
|
+
* asked, and answers `REVIEW_REQUIRED` when a review is required and missing.
|
|
23
|
+
* The first is the operator's own omission — nobody is coming — and the second
|
|
24
|
+
* is somebody else's turn.
|
|
25
|
+
*/
|
|
26
|
+
export const prStatuses = [
|
|
27
|
+
"approved",
|
|
28
|
+
"changesRequested",
|
|
29
|
+
"closed",
|
|
30
|
+
"draft",
|
|
31
|
+
"merged",
|
|
32
|
+
"noReviewers",
|
|
33
|
+
"waitingReview",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export type PrStatus = (typeof prStatuses)[number];
|
|
37
|
+
|
|
38
|
+
const reviewStatuses: Record<
|
|
39
|
+
NonNullable<PrReviewDecision>,
|
|
40
|
+
Extract<PrStatus, "approved" | "changesRequested" | "waitingReview">
|
|
41
|
+
> = {
|
|
42
|
+
approved: "approved",
|
|
43
|
+
changesRequested: "changesRequested",
|
|
44
|
+
reviewRequired: "waitingReview",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A closed or merged PR keeps its own status whatever the last review said: an
|
|
49
|
+
* approved PR that was closed is closed, and saying "approved" would describe a
|
|
50
|
+
* decision nobody is going to act on.
|
|
51
|
+
*/
|
|
52
|
+
export const prStatus = ({
|
|
53
|
+
state,
|
|
54
|
+
reviewDecision,
|
|
55
|
+
}: {
|
|
56
|
+
state: PrState;
|
|
57
|
+
reviewDecision: PrReviewDecision;
|
|
58
|
+
}): PrStatus => {
|
|
59
|
+
if (state !== "open") return state;
|
|
60
|
+
return reviewDecision === null
|
|
61
|
+
? "noReviewers"
|
|
62
|
+
: reviewStatuses[reviewDecision];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const prStatusLabels: Record<PrStatus, string> = {
|
|
66
|
+
approved: "approved",
|
|
67
|
+
changesRequested: "changes requested",
|
|
68
|
+
closed: "closed",
|
|
69
|
+
draft: "draft",
|
|
70
|
+
merged: "merged",
|
|
71
|
+
noReviewers: "no reviewers",
|
|
72
|
+
waitingReview: "waiting review",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Only while it can still be merged: GitHub keeps answering for a closed one,
|
|
77
|
+
* and a conflict nobody will resolve is not something to look at.
|
|
78
|
+
*/
|
|
79
|
+
export const prHasConflicts = ({
|
|
80
|
+
state,
|
|
81
|
+
mergeable,
|
|
82
|
+
}: {
|
|
83
|
+
state: PrState;
|
|
84
|
+
mergeable: PrMergeable;
|
|
85
|
+
}): boolean =>
|
|
86
|
+
(state === "open" || state === "draft") && mergeable === "conflicting";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Built rather than stored: the number and `owner/repo` are kept apart,
|
|
90
|
+
* and a url column would be a third copy of both that goes stale when either
|
|
91
|
+
* moves. GitHub redirects `/pull/<n>` to the right kind of page, so nothing here
|
|
92
|
+
* has to know whether the number belongs to a PR or an issue.
|
|
93
|
+
*/
|
|
94
|
+
export const prUrl = (ownerRepo: string, number: number): string =>
|
|
95
|
+
`https://github.com/${ownerRepo}/pull/${String(number)}`;
|
|
96
|
+
|
|
97
|
+
const prNumberPattern = /^#?(\d+)$/;
|
|
98
|
+
const prUrlPattern =
|
|
99
|
+
/^https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#]\S*)?$/i;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A pull request of `ownerRepo` as the operator names one — `123`, `#123` or
|
|
103
|
+
* its GitHub url, a tab of it included — or why the text names none. A url of
|
|
104
|
+
* another repository is refused rather than reduced to its number, which would
|
|
105
|
+
* name a different pull request.
|
|
106
|
+
*/
|
|
107
|
+
export const prNumberOf = (
|
|
108
|
+
input: string,
|
|
109
|
+
ownerRepo: string,
|
|
110
|
+
): number | string => {
|
|
111
|
+
const text = input.trim();
|
|
112
|
+
const url = prUrlPattern.exec(text);
|
|
113
|
+
const [, digits] = url === null ? (prNumberPattern.exec(text) ?? []) : [];
|
|
114
|
+
if (url !== null && url[1]?.toLowerCase() !== ownerRepo.toLowerCase()) {
|
|
115
|
+
return `that pull request is not in ${ownerRepo}`;
|
|
116
|
+
}
|
|
117
|
+
const number = Number(url?.[2] ?? digits);
|
|
118
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
119
|
+
return "a pull request number or its GitHub link is expected";
|
|
120
|
+
}
|
|
121
|
+
return number;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* reviewflow's review state in GitHub's vocabulary, which `pr` stores. `none` is
|
|
126
|
+
* what GitHub cannot say on a repository that requires a review: it answers
|
|
127
|
+
* `REVIEW_REQUIRED` whether or not anyone was asked, where reviewflow tells the
|
|
128
|
+
* two apart.
|
|
129
|
+
*/
|
|
130
|
+
export const reviewflowReviewDecisions: Record<
|
|
131
|
+
ReviewflowReviewState,
|
|
132
|
+
PrReviewDecision
|
|
133
|
+
> = {
|
|
134
|
+
approved: "approved",
|
|
135
|
+
changesRequested: "changesRequested",
|
|
136
|
+
requested: "reviewRequired",
|
|
137
|
+
none: null,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** null when reviewflow does not know enough to say */
|
|
141
|
+
export const reviewflowPrStatus = ({
|
|
142
|
+
state,
|
|
143
|
+
reviewState,
|
|
144
|
+
}: Pick<ReviewflowBoardPr, "reviewState" | "state">): PrStatus | null => {
|
|
145
|
+
if (state === null) return null;
|
|
146
|
+
if (state !== "open") return state;
|
|
147
|
+
if (reviewState === null) return null;
|
|
148
|
+
return prStatus({
|
|
149
|
+
state,
|
|
150
|
+
reviewDecision: reviewflowReviewDecisions[reviewState],
|
|
151
|
+
});
|
|
152
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A past date the way the operator says it: "today 14:32", "yesterday 09:10",
|
|
3
|
+
* "last Monday 16:05", then "12 Oct, 14:32" — and the year once it is not this
|
|
4
|
+
* one. The boards read dates to answer "is this from this morning or last
|
|
5
|
+
* week", which a full timestamp makes them work out.
|
|
6
|
+
*
|
|
7
|
+
* Calendar days in the machine's own time zone, not 24-hour spans: 23:50
|
|
8
|
+
* yesterday is "yesterday" at 00:10. The time and the month follow the
|
|
9
|
+
* locale; the words are the app's, which is in English.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/** how many days back the weekday alone still names the day without doubt */
|
|
15
|
+
const weekdayReach = 6;
|
|
16
|
+
|
|
17
|
+
const startOfDay = (date: Date): number =>
|
|
18
|
+
new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
19
|
+
|
|
20
|
+
/** Whole calendar days from `date` to `now`; negative for a future date. */
|
|
21
|
+
const calendarDaysAgo = (date: Date, now: Date): number =>
|
|
22
|
+
Math.round((startOfDay(now) - startOfDay(date)) / dayMs);
|
|
23
|
+
|
|
24
|
+
export interface FormatRelativeDateOptions {
|
|
25
|
+
now?: Date;
|
|
26
|
+
/** a BCP 47 tag; the runtime's own when absent */
|
|
27
|
+
locale?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const formatRelativeDate = (
|
|
31
|
+
value: Date | string,
|
|
32
|
+
{ now = new Date(), locale }: FormatRelativeDateOptions = {},
|
|
33
|
+
): string => {
|
|
34
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
35
|
+
if (Number.isNaN(date.getTime())) return "unknown date";
|
|
36
|
+
|
|
37
|
+
const time = date.toLocaleTimeString(locale, {
|
|
38
|
+
hour: "2-digit",
|
|
39
|
+
minute: "2-digit",
|
|
40
|
+
});
|
|
41
|
+
const days = calendarDaysAgo(date, now);
|
|
42
|
+
|
|
43
|
+
if (days === 0) return `today ${time}`;
|
|
44
|
+
if (days === 1) return `yesterday ${time}`;
|
|
45
|
+
if (days > 1 && days <= weekdayReach) {
|
|
46
|
+
const weekday = date.toLocaleDateString("en", { weekday: "long" });
|
|
47
|
+
return `last ${weekday} ${time}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const day = date.toLocaleDateString(locale, {
|
|
51
|
+
day: "numeric",
|
|
52
|
+
month: "short",
|
|
53
|
+
...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
|
|
54
|
+
});
|
|
55
|
+
return `${day}, ${time}`;
|
|
56
|
+
};
|