opencode-forgekeeper 2.1.0 → 2.2.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/README.md +39 -14
- package/dist/index.js +543 -193
- package/dist/tui.js +263 -29
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -73,14 +73,182 @@ class ForgeError extends Error {
|
|
|
73
73
|
var query = (text) => text;
|
|
74
74
|
var typed = (transport) => async (host, query2, variables) => await transport(host, query2, variables);
|
|
75
75
|
|
|
76
|
-
// packages/forgekeeper/src/
|
|
76
|
+
// packages/forgekeeper/src/github-feedback.ts
|
|
77
77
|
var PAGE_SIZE = 100;
|
|
78
78
|
var MAX_COMMENT_PAGES = 20;
|
|
79
|
+
var MAX_FETCH_REQUESTS = 200;
|
|
80
|
+
var author = "author { __typename login ... on User { id } ... on Bot { id } ... on Mannequin { id } }";
|
|
81
|
+
var page = "pageInfo { hasPreviousPage startCursor }";
|
|
82
|
+
var INLINE_FIELDS = `id url state createdAt publishedAt lastEditedAt isMinimized ${author}`;
|
|
83
|
+
var listQuery = (field, connection) => query(`query($owner: String!, $repo: String!, $number: Int!, $before: String) {
|
|
84
|
+
viewer { id }
|
|
85
|
+
repository(owner: $owner, name: $repo) {
|
|
86
|
+
pullRequest(number: $number) {
|
|
87
|
+
${field}(last: ${PAGE_SIZE}, before: $before) { ${page} nodes { ${connection} } }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}`);
|
|
91
|
+
var ISSUE_COMMENTS_QUERY = listQuery("comments", `id url createdAt publishedAt lastEditedAt isMinimized ${author}`);
|
|
92
|
+
var REVIEWS_QUERY = listQuery("reviews", `id url state body submittedAt lastEditedAt isMinimized ${author}`);
|
|
93
|
+
var THREADS_QUERY = listQuery("reviewThreads", `id isResolved comments(last: ${PAGE_SIZE}) { ${page} nodes { ${INLINE_FIELDS} } }`);
|
|
94
|
+
var THREAD_COMMENTS_QUERY = query(`query($id: ID!, $before: String) {
|
|
95
|
+
node(id: $id) {
|
|
96
|
+
... on PullRequestReviewThread {
|
|
97
|
+
comments(last: ${PAGE_SIZE}, before: $before) { ${page} nodes { ${INLINE_FIELDS} } }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}`);
|
|
101
|
+
var REVIEW_STATES = new Set(["COMMENTED", "CHANGES_REQUESTED", "APPROVED"]);
|
|
102
|
+
var time = (...values) => {
|
|
103
|
+
const value = values.find((candidate) => candidate);
|
|
104
|
+
return value ? Date.parse(value) : NaN;
|
|
105
|
+
};
|
|
106
|
+
var activity = (lastEditedAt, postedAt) => {
|
|
107
|
+
const edited = lastEditedAt ? Date.parse(lastEditedAt) : NaN;
|
|
108
|
+
return Number.isFinite(edited) ? Math.max(edited, postedAt) : postedAt;
|
|
109
|
+
};
|
|
110
|
+
function build({ node, postedAt, editedAt, resolved }) {
|
|
111
|
+
if (node.isMinimized || !node.author?.id || !Number.isFinite(postedAt) || !Number.isFinite(editedAt))
|
|
112
|
+
return;
|
|
113
|
+
const { __typename, login } = node.author;
|
|
114
|
+
return {
|
|
115
|
+
id: node.id,
|
|
116
|
+
order: postedAt,
|
|
117
|
+
url: node.url ?? "",
|
|
118
|
+
editedAt,
|
|
119
|
+
resolved,
|
|
120
|
+
bot: __typename === "Bot" || login.endsWith("[bot]"),
|
|
121
|
+
authorID: node.author.id,
|
|
122
|
+
username: login
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function toIssueComment(node) {
|
|
126
|
+
const postedAt = time(node.publishedAt, node.createdAt);
|
|
127
|
+
return build({
|
|
128
|
+
node,
|
|
129
|
+
postedAt,
|
|
130
|
+
editedAt: activity(node.lastEditedAt, postedAt),
|
|
131
|
+
resolved: false
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function toReview(node) {
|
|
135
|
+
if (!REVIEW_STATES.has(node.state) || !node.body.trim())
|
|
136
|
+
return;
|
|
137
|
+
const postedAt = time(node.submittedAt);
|
|
138
|
+
return build({
|
|
139
|
+
node,
|
|
140
|
+
postedAt,
|
|
141
|
+
editedAt: activity(node.lastEditedAt, postedAt),
|
|
142
|
+
resolved: false
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function toInlineComment(node, resolved) {
|
|
146
|
+
if (node.state === "PENDING")
|
|
147
|
+
return;
|
|
148
|
+
const postedAt = time(node.publishedAt, node.createdAt);
|
|
149
|
+
return build({
|
|
150
|
+
node,
|
|
151
|
+
postedAt,
|
|
152
|
+
editedAt: activity(node.lastEditedAt, postedAt),
|
|
153
|
+
resolved
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
var commentWindow = { pages: MAX_COMMENT_PAGES, completeAtLimit: true };
|
|
157
|
+
var allThreads = { pages: MAX_FETCH_REQUESTS, completeAtLimit: false };
|
|
158
|
+
async function readList(graphql, host, base, text, field, limit = commentWindow) {
|
|
159
|
+
const pages = [];
|
|
160
|
+
let viewer;
|
|
161
|
+
let before;
|
|
162
|
+
const result = (complete) => ({
|
|
163
|
+
items: pages.toReversed().flat(),
|
|
164
|
+
viewer,
|
|
165
|
+
complete
|
|
166
|
+
});
|
|
167
|
+
for (let count = 0;count < limit.pages; count++) {
|
|
168
|
+
const variables = before ? { ...base, before } : base;
|
|
169
|
+
const data = await graphql(host, text, variables);
|
|
170
|
+
viewer ??= data.viewer?.id ?? undefined;
|
|
171
|
+
const connection = data.repository?.pullRequest?.[field];
|
|
172
|
+
if (!connection)
|
|
173
|
+
return count === 0 ? { viewer, complete: false } : result(false);
|
|
174
|
+
pages.push(connection.nodes);
|
|
175
|
+
if (!connection.pageInfo.hasPreviousPage)
|
|
176
|
+
return result(true);
|
|
177
|
+
before = connection.pageInfo.startCursor ?? undefined;
|
|
178
|
+
if (!before)
|
|
179
|
+
return result(false);
|
|
180
|
+
}
|
|
181
|
+
return result(limit.completeAtLimit);
|
|
182
|
+
}
|
|
183
|
+
async function readOlderComments(graphql, host, thread) {
|
|
184
|
+
if (!thread.comments)
|
|
185
|
+
return { comments: [], complete: false };
|
|
186
|
+
const pages = [];
|
|
187
|
+
let before = thread.comments?.pageInfo.hasPreviousPage ? thread.comments.pageInfo.startCursor ?? undefined : undefined;
|
|
188
|
+
if (thread.comments?.pageInfo.hasPreviousPage && !before)
|
|
189
|
+
return { comments: [], complete: false };
|
|
190
|
+
for (let count = 1;before && count < MAX_COMMENT_PAGES; count++) {
|
|
191
|
+
const data = await graphql(host, THREAD_COMMENTS_QUERY, { id: thread.id, before });
|
|
192
|
+
const connection = data.node?.comments;
|
|
193
|
+
if (!connection)
|
|
194
|
+
return { comments: pages.toReversed().flat(), complete: false };
|
|
195
|
+
pages.push(connection.nodes);
|
|
196
|
+
before = connection.pageInfo.hasPreviousPage ? connection.pageInfo.startCursor ?? undefined : undefined;
|
|
197
|
+
if (connection.pageInfo.hasPreviousPage && !before)
|
|
198
|
+
return { comments: pages.toReversed().flat(), complete: false };
|
|
199
|
+
}
|
|
200
|
+
return { comments: pages.toReversed().flat(), complete: true };
|
|
201
|
+
}
|
|
202
|
+
async function fetchComments(transport, host, project, number) {
|
|
203
|
+
let requests = 0;
|
|
204
|
+
const graphql = typed(async (...args) => {
|
|
205
|
+
if (requests++ >= MAX_FETCH_REQUESTS)
|
|
206
|
+
return {};
|
|
207
|
+
return transport(...args);
|
|
208
|
+
});
|
|
209
|
+
const [owner, repo] = project.split("/");
|
|
210
|
+
const base = { owner, repo, number: Number(number) };
|
|
211
|
+
const [conversation, reviews, threads] = await Promise.all([
|
|
212
|
+
readList(graphql, host, base, ISSUE_COMMENTS_QUERY, "comments"),
|
|
213
|
+
readList(graphql, host, base, REVIEWS_QUERY, "reviews"),
|
|
214
|
+
readList(graphql, host, base, THREADS_QUERY, "reviewThreads", allThreads)
|
|
215
|
+
]);
|
|
216
|
+
if (!conversation.items || !reviews.items || !threads.items)
|
|
217
|
+
return;
|
|
218
|
+
const viewerID = conversation.viewer ?? reviews.viewer ?? threads.viewer;
|
|
219
|
+
let threadsComplete = threads.complete;
|
|
220
|
+
const inline = [];
|
|
221
|
+
for (const thread of threads.items) {
|
|
222
|
+
const older = await readOlderComments(graphql, host, thread);
|
|
223
|
+
threadsComplete &&= older.complete;
|
|
224
|
+
for (const node of [...older.comments, ...thread.comments?.nodes ?? []]) {
|
|
225
|
+
const comment = toInlineComment(node, thread.isResolved);
|
|
226
|
+
if (comment)
|
|
227
|
+
inline.push(comment);
|
|
228
|
+
}
|
|
229
|
+
inline.sort((a, b) => b.order - a.order);
|
|
230
|
+
inline.splice(PAGE_SIZE * MAX_COMMENT_PAGES);
|
|
231
|
+
}
|
|
232
|
+
const comments = [
|
|
233
|
+
...conversation.items.map(toIssueComment),
|
|
234
|
+
...reviews.items.map(toReview),
|
|
235
|
+
...inline.sort((a, b) => a.order - b.order).slice(-PAGE_SIZE * MAX_COMMENT_PAGES)
|
|
236
|
+
].flatMap((comment) => comment ?? []);
|
|
237
|
+
return {
|
|
238
|
+
complete: conversation.complete && reviews.complete && threadsComplete && viewerID !== undefined,
|
|
239
|
+
viewerID,
|
|
240
|
+
comments: comments.sort((a, b) => a.order - b.order)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// packages/forgekeeper/src/gitlab-feedback.ts
|
|
245
|
+
var PAGE_SIZE2 = 100;
|
|
246
|
+
var MAX_COMMENT_PAGES2 = 20;
|
|
79
247
|
var COMMENTS_QUERY = query(`query($project: ID!, $iid: String!, $before: String) {
|
|
80
248
|
currentUser { id }
|
|
81
249
|
project(fullPath: $project) {
|
|
82
250
|
mergeRequest(iid: $iid) {
|
|
83
|
-
notes(last: ${
|
|
251
|
+
notes(last: ${PAGE_SIZE2}, before: $before, filter: ONLY_COMMENTS) {
|
|
84
252
|
pageInfo { hasPreviousPage startCursor }
|
|
85
253
|
nodes {
|
|
86
254
|
id url createdAt updatedAt lastEditedAt system
|
|
@@ -109,7 +277,7 @@ function toComment(node) {
|
|
|
109
277
|
username: node.author.username
|
|
110
278
|
};
|
|
111
279
|
}
|
|
112
|
-
async function
|
|
280
|
+
async function fetchComments2(transport, host, project, iid) {
|
|
113
281
|
const graphql = typed(transport);
|
|
114
282
|
const pages = [];
|
|
115
283
|
let viewerID;
|
|
@@ -119,7 +287,7 @@ async function fetchComments(transport, host, project, iid) {
|
|
|
119
287
|
viewerID,
|
|
120
288
|
comments: pages.toReversed().flat()
|
|
121
289
|
});
|
|
122
|
-
for (let
|
|
290
|
+
for (let page2 = 0;page2 < MAX_COMMENT_PAGES2; page2++) {
|
|
123
291
|
const variables = { project, iid };
|
|
124
292
|
if (before)
|
|
125
293
|
variables.before = before;
|
|
@@ -127,7 +295,7 @@ async function fetchComments(transport, host, project, iid) {
|
|
|
127
295
|
viewerID ??= data.currentUser?.id ?? undefined;
|
|
128
296
|
const connection = data.project?.mergeRequest?.notes;
|
|
129
297
|
if (!connection)
|
|
130
|
-
return
|
|
298
|
+
return page2 === 0 ? undefined : snapshot(false);
|
|
131
299
|
pages.push(connection.nodes.flatMap((node) => toComment(node) ?? []));
|
|
132
300
|
if (!connection.pageInfo.hasPreviousPage)
|
|
133
301
|
return snapshot(true);
|
|
@@ -140,7 +308,7 @@ async function fetchComments(transport, host, project, iid) {
|
|
|
140
308
|
|
|
141
309
|
// packages/forgekeeper/src/gitlab.ts
|
|
142
310
|
var DISCUSSIONS = "pageInfo { hasNextPage endCursor } nodes { resolvable resolved }";
|
|
143
|
-
var
|
|
311
|
+
var PAGE_SIZE3 = 100;
|
|
144
312
|
var MAX_DISCUSSION_PAGES = 50;
|
|
145
313
|
var MERGE_REQUEST_FIELDS = `
|
|
146
314
|
iid title webUrl state draft conflicts approved detailedMergeStatus diffHeadSha updatedAt targetBranch
|
|
@@ -149,7 +317,7 @@ var MERGE_REQUEST_FIELDS = `
|
|
|
149
317
|
targetProject { fullPath }
|
|
150
318
|
headPipeline { status path detailedStatus { label } }
|
|
151
319
|
reviewers(first: 100) { nodes { type username bot mergeRequestInteraction { reviewState approved } } }
|
|
152
|
-
discussions(first: ${
|
|
320
|
+
discussions(first: ${PAGE_SIZE3}) { ${DISCUSSIONS} }
|
|
153
321
|
`;
|
|
154
322
|
function mergeRequestsQuery(projectCount) {
|
|
155
323
|
const variables = Array.from({ length: projectCount }, (_, index) => `$p${index}: ID!`).join(", ");
|
|
@@ -184,7 +352,7 @@ var SOURCE_BRANCH_QUERY = query(`query($project: ID!, $iid: String!) {
|
|
|
184
352
|
}`);
|
|
185
353
|
var DISCUSSIONS_QUERY = query(`query($project: ID!, $iid: String!, $after: String) {
|
|
186
354
|
project(fullPath: $project) {
|
|
187
|
-
mergeRequest(iid: $iid) { discussions(first: ${
|
|
355
|
+
mergeRequest(iid: $iid) { discussions(first: ${PAGE_SIZE3}, after: $after) { ${DISCUSSIONS} } }
|
|
188
356
|
}
|
|
189
357
|
}`);
|
|
190
358
|
function classifyError(message, code) {
|
|
@@ -223,7 +391,7 @@ function glabGraphQL(run, cwd) {
|
|
|
223
391
|
return body.data;
|
|
224
392
|
};
|
|
225
393
|
}
|
|
226
|
-
var countUnresolved = (
|
|
394
|
+
var countUnresolved = (page2) => page2.nodes.filter((discussion) => discussion.resolvable && !discussion.resolved).length;
|
|
227
395
|
var gitlabTraits = {
|
|
228
396
|
kind: "gitlab",
|
|
229
397
|
noun: "MR",
|
|
@@ -258,7 +426,7 @@ var gitlabTraits = {
|
|
|
258
426
|
feedback: {
|
|
259
427
|
async fetch(run, directory, request) {
|
|
260
428
|
const ref = gitlabTraits.parseUrl(request.url);
|
|
261
|
-
return ref ?
|
|
429
|
+
return ref ? fetchComments2(glabGraphQL(run, directory), ref.host, ref.project, ref.iid) : undefined;
|
|
262
430
|
}
|
|
263
431
|
}
|
|
264
432
|
};
|
|
@@ -330,22 +498,22 @@ class GitLabForge {
|
|
|
330
498
|
}
|
|
331
499
|
async unresolvedThreads(host, node) {
|
|
332
500
|
let count = countUnresolved(node.discussions);
|
|
333
|
-
let
|
|
501
|
+
let page2 = node.discussions.pageInfo;
|
|
334
502
|
let pages = 1;
|
|
335
|
-
while (
|
|
503
|
+
while (page2.hasNextPage && page2.endCursor && pages < MAX_DISCUSSION_PAGES) {
|
|
336
504
|
const data = await this.graphql(host, DISCUSSIONS_QUERY, {
|
|
337
505
|
project: node.targetProject.fullPath,
|
|
338
506
|
iid: node.iid,
|
|
339
|
-
after:
|
|
507
|
+
after: page2.endCursor
|
|
340
508
|
});
|
|
341
509
|
const next = data.project?.mergeRequest?.discussions;
|
|
342
510
|
if (!next)
|
|
343
511
|
break;
|
|
344
512
|
count += countUnresolved(next);
|
|
345
|
-
|
|
513
|
+
page2 = next.pageInfo;
|
|
346
514
|
pages++;
|
|
347
515
|
}
|
|
348
|
-
return { count, complete: !
|
|
516
|
+
return { count, complete: !page2.hasNextPage };
|
|
349
517
|
}
|
|
350
518
|
async normalize(host, node) {
|
|
351
519
|
const threads = await this.unresolvedThreads(host, node);
|
|
@@ -384,10 +552,12 @@ class GitLabForge {
|
|
|
384
552
|
}
|
|
385
553
|
|
|
386
554
|
// packages/forgekeeper/src/github.ts
|
|
555
|
+
var COPILOT_AUTHOR = "copilot-pull-request-reviewer[bot]";
|
|
387
556
|
var fields = `number title url state isDraft mergeable reviewDecision headRefOid headRefName
|
|
388
557
|
headRepository { nameWithOwner } baseRepository { nameWithOwner } baseRefName updatedAt
|
|
389
558
|
reviewThreads(first:100) { nodes { isResolved } pageInfo { hasNextPage endCursor } }
|
|
390
|
-
reviewRequests(first:100) { nodes { requestedReviewer { ... on User { login } ... on Team { name } } } pageInfo { hasNextPage } }
|
|
559
|
+
reviewRequests(first:100) { nodes { requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { name } } } pageInfo { hasNextPage endCursor } }
|
|
560
|
+
copilotReviews: reviews(last:1, author:"${COPILOT_AUTHOR}", states:[COMMENTED,CHANGES_REQUESTED,APPROVED,DISMISSED]) { nodes { id state comments { totalCount } } }
|
|
391
561
|
commits(last:1) { nodes { commit { statusCheckRollup { contexts(first:100) {
|
|
392
562
|
nodes { __typename ... on CheckRun { status conclusion detailsUrl } ... on StatusContext { state targetUrl } }
|
|
393
563
|
pageInfo { hasNextPage endCursor }
|
|
@@ -481,8 +651,43 @@ var githubTraits = {
|
|
|
481
651
|
indicators: (request) => request.reviewDecision === "CHANGES_REQUESTED" ? [{ text: "changes requested", tone: "warning", essential: true }] : [],
|
|
482
652
|
details: (request) => [
|
|
483
653
|
`Review decision: ${request.reviewDecision ? humanize(request.reviewDecision) : "not reported"}`
|
|
484
|
-
]
|
|
654
|
+
],
|
|
655
|
+
automatedReview: {
|
|
656
|
+
name: "GitHub Copilot",
|
|
657
|
+
status: (request) => copilotStatus(request.copilotReview)
|
|
658
|
+
},
|
|
659
|
+
feedback: {
|
|
660
|
+
async fetch(run, directory, request) {
|
|
661
|
+
const ref = githubTraits.parseUrl(request.url);
|
|
662
|
+
return ref ? fetchComments(ghGraphQL(run, directory), ref.host, ref.project, ref.iid) : undefined;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
485
665
|
};
|
|
666
|
+
function copilotStatus(review) {
|
|
667
|
+
if (!review)
|
|
668
|
+
return;
|
|
669
|
+
const feedback = review.comments > 0 && !review.dismissed;
|
|
670
|
+
const label = review.dismissed ? "dismissed" : feedback ? "commented" : "no comments";
|
|
671
|
+
const completed = review.reviewID ? { id: review.reviewID, feedback, label } : undefined;
|
|
672
|
+
if (review.requested)
|
|
673
|
+
return { state: "running", label: "reviewing", completed };
|
|
674
|
+
if (!completed)
|
|
675
|
+
return;
|
|
676
|
+
return { state: feedback ? "feedback" : "settled", label, completed };
|
|
677
|
+
}
|
|
678
|
+
var isCopilotReviewer = (login) => /^copilot-pull-request-reviewer(\[bot\])?$/i.test(login ?? "");
|
|
679
|
+
function copilotReview(node) {
|
|
680
|
+
const requested = node.reviewRequests.nodes.some((request) => isCopilotReviewer(request.requestedReviewer?.login));
|
|
681
|
+
const latest = node.copilotReviews?.nodes?.find((review) => review);
|
|
682
|
+
if (!requested && !latest)
|
|
683
|
+
return;
|
|
684
|
+
return {
|
|
685
|
+
requested,
|
|
686
|
+
reviewID: latest?.id,
|
|
687
|
+
comments: latest?.comments.totalCount ?? 0,
|
|
688
|
+
...latest?.state === "DISMISSED" ? { dismissed: true } : {}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
486
691
|
|
|
487
692
|
class GitHubForge {
|
|
488
693
|
traits = githubTraits;
|
|
@@ -533,7 +738,7 @@ class GitHubForge {
|
|
|
533
738
|
const [owner, repo] = target.path.split("/");
|
|
534
739
|
let after;
|
|
535
740
|
let complete = false;
|
|
536
|
-
for (let
|
|
741
|
+
for (let page2 = 0;page2 < MAX_PAGES; page2++) {
|
|
537
742
|
const data = await this.graphql(target.host, query(`query($owner:String!,$repo:String!,$branch:String!${after ? ",$after:String!" : ""}) {
|
|
538
743
|
repository(owner:$owner,name:$repo) { pullRequests(first:100,states:OPEN,headRefName:$branch${after ? ",after:$after" : ""}) {
|
|
539
744
|
nodes { ${fields} } pageInfo { hasNextPage endCursor }
|
|
@@ -563,21 +768,21 @@ class GitHubForge {
|
|
|
563
768
|
async threads(host, node) {
|
|
564
769
|
const [owner, repo] = node.baseRepository.nameWithOwner.split("/");
|
|
565
770
|
const threads = [...node.reviewThreads.nodes];
|
|
566
|
-
let
|
|
567
|
-
for (let count = 1;
|
|
771
|
+
let page2 = node.reviewThreads.pageInfo;
|
|
772
|
+
for (let count = 1;page2?.hasNextPage && page2.endCursor && count < MAX_PAGES; count++) {
|
|
568
773
|
const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
|
|
569
774
|
repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
|
|
570
775
|
reviewThreads(first:100,after:$after) { nodes { isResolved } pageInfo { hasNextPage endCursor } }
|
|
571
|
-
} } }`), { owner, repo, number: node.number, after:
|
|
776
|
+
} } }`), { owner, repo, number: node.number, after: page2.endCursor });
|
|
572
777
|
const next = data.repository?.pullRequest?.reviewThreads;
|
|
573
778
|
if (!next)
|
|
574
779
|
break;
|
|
575
780
|
threads.push(...next.nodes);
|
|
576
|
-
|
|
781
|
+
page2 = next.pageInfo;
|
|
577
782
|
}
|
|
578
783
|
return {
|
|
579
784
|
unresolved: threads.filter((thread) => !thread.isResolved).length,
|
|
580
|
-
complete: !
|
|
785
|
+
complete: !page2?.hasNextPage
|
|
581
786
|
};
|
|
582
787
|
}
|
|
583
788
|
async contexts(host, node) {
|
|
@@ -598,7 +803,33 @@ class GitHubForge {
|
|
|
598
803
|
}
|
|
599
804
|
return { nodes: contexts, complete: !cursor?.hasNextPage };
|
|
600
805
|
}
|
|
806
|
+
async reviewers(host, node) {
|
|
807
|
+
const nodes = [...node.reviewRequests.nodes];
|
|
808
|
+
let reviewerPage = node.reviewRequests.pageInfo;
|
|
809
|
+
for (let count = 1;reviewerPage?.hasNextPage && count < MAX_PAGES; count++) {
|
|
810
|
+
if (!reviewerPage.endCursor)
|
|
811
|
+
throw new ForgeError("request", "GitHub reviewer pagination cursor missing");
|
|
812
|
+
const [owner, repo] = node.baseRepository.nameWithOwner.split("/");
|
|
813
|
+
const data = await this.graphql(host, query(`query($owner:String!,$repo:String!,$number:Int!,$after:String!) {
|
|
814
|
+
repository(owner:$owner,name:$repo) { pullRequest(number:$number) {
|
|
815
|
+
reviewRequests(first:100,after:$after) {
|
|
816
|
+
nodes { requestedReviewer { __typename ... on User { login } ... on Bot { login } ... on Team { name } } }
|
|
817
|
+
pageInfo { hasNextPage endCursor }
|
|
818
|
+
}
|
|
819
|
+
} }
|
|
820
|
+
}`), { owner, repo, number: node.number, after: reviewerPage.endCursor });
|
|
821
|
+
const next = data.repository?.pullRequest?.reviewRequests;
|
|
822
|
+
if (!next)
|
|
823
|
+
throw new ForgeError("request", "GitHub reviewers unavailable");
|
|
824
|
+
nodes.push(...next.nodes);
|
|
825
|
+
reviewerPage = next.pageInfo;
|
|
826
|
+
}
|
|
827
|
+
if (reviewerPage?.hasNextPage)
|
|
828
|
+
throw new ForgeError("request", "GitHub reviewers exceeded the pagination limit");
|
|
829
|
+
return { nodes, pageInfo: reviewerPage };
|
|
830
|
+
}
|
|
601
831
|
async normalize(host, node) {
|
|
832
|
+
const reviewers = await this.reviewers(host, node);
|
|
602
833
|
const [threads, contexts] = [await this.threads(host, node), await this.contexts(host, node)];
|
|
603
834
|
return {
|
|
604
835
|
forge: "github",
|
|
@@ -611,11 +842,14 @@ class GitHubForge {
|
|
|
611
842
|
conflictsKnown: node.mergeable !== "UNKNOWN",
|
|
612
843
|
approved: node.reviewDecision === "APPROVED",
|
|
613
844
|
reviewDecision: node.reviewDecision ?? undefined,
|
|
614
|
-
|
|
845
|
+
copilotReview: copilotReview({ ...node, reviewRequests: reviewers }),
|
|
846
|
+
awaitingReviewers: reviewers.nodes.flatMap((request) => {
|
|
615
847
|
const reviewer = request.requestedReviewer;
|
|
848
|
+
if (reviewer?.__typename === "Bot")
|
|
849
|
+
return [];
|
|
616
850
|
return reviewer?.login ? [reviewer.login] : reviewer?.name ? [reviewer.name] : [];
|
|
617
851
|
}),
|
|
618
|
-
reviewersComplete: !
|
|
852
|
+
reviewersComplete: !reviewers.pageInfo?.hasNextPage,
|
|
619
853
|
targetProject: node.baseRepository.nameWithOwner,
|
|
620
854
|
targetBranch: node.baseRefName,
|
|
621
855
|
headSha: node.headRefOid,
|
|
@@ -731,7 +965,7 @@ function footerSegments(snapshot) {
|
|
|
731
965
|
const { lookup, error } = snapshot;
|
|
732
966
|
const stale = error ? [{ text: "stale", tone: "warning", essential: true }] : [];
|
|
733
967
|
if (!lookup || lookup.kind === "none" || lookup.requests.length === 0) {
|
|
734
|
-
return error ? [{ text: "PR/MR status unavailable", tone: "warning" }] : [];
|
|
968
|
+
return snapshot.targeted && (error || lookup?.explicitTarget) ? [{ text: "PR/MR status unavailable", tone: "warning" }] : [];
|
|
735
969
|
}
|
|
736
970
|
if (lookup.failed?.length) {
|
|
737
971
|
stale.unshift({
|
|
@@ -808,7 +1042,7 @@ function responsiveFooterSegments(snapshot, width) {
|
|
|
808
1042
|
}
|
|
809
1043
|
return fitted;
|
|
810
1044
|
}
|
|
811
|
-
var
|
|
1045
|
+
var time2 = (value) => new Date(value).toLocaleTimeString();
|
|
812
1046
|
function describe(request, head) {
|
|
813
1047
|
const traits = traitsOf(request);
|
|
814
1048
|
const lines = [
|
|
@@ -856,9 +1090,9 @@ function detailsMessage(snapshot) {
|
|
|
856
1090
|
lines.push("", ...describe(request, repository?.head ?? ""));
|
|
857
1091
|
}
|
|
858
1092
|
if (error)
|
|
859
|
-
lines.push("", `Last refresh failed at ${
|
|
1093
|
+
lines.push("", `Last refresh failed at ${time2(error.at)}: ${error.message}`);
|
|
860
1094
|
if (fetchedAt !== undefined)
|
|
861
|
-
lines.push("", `Last fetched: ${
|
|
1095
|
+
lines.push("", `Last fetched: ${time2(fetchedAt)}`);
|
|
862
1096
|
return lines.join(`
|
|
863
1097
|
`);
|
|
864
1098
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-forgekeeper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "OpenCode plugin that tracks each session's GitHub and GitLab issues and PRs/MRs: title prefixes, a PR/MR status footer, and review notifications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pedro Pombeiro",
|