| author | |
| committer | |
| log | 21eb9a421aff7a8fbe044b1a34bb3e364fb94a68 |
| tree | 61de166464a3f95c63ea4785b264cb51cc021ef5 |
| parent | f973b1d7d5cd7ffb4dd3c1c61cc0caece09fd1a6 |
| signature | Signed by PGP key B5690EEEBB952194 |
8 files changed, 89 insertions(+), 10 deletions(-)
apps/server/src/orchestration-v2/pullRequestWatch.test.ts+21-1| ... | @@ -1,7 +1,6 @@ | ... | @@ -1,7 +1,6 @@ |
| 1 | import type { | 1 | import type { |
| 2 | PullRequestCheck, | 2 | PullRequestCheck, |
| 3 | PullRequestComment, | 3 | PullRequestComment, |
| 4 | PullRequestDetail, | ||
| 5 | ThreadPullRequestWatch, | 4 | ThreadPullRequestWatch, |
| 6 | } from "@t3tools/contracts"; | 5 | } from "@t3tools/contracts"; |
| 7 | import { assert, describe, it } from "@effect/vitest"; | 6 | import { assert, describe, it } from "@effect/vitest"; |
| ... | @@ -119,6 +118,27 @@ describe("evaluatePullRequestWatch", () => { | ... | @@ -119,6 +118,27 @@ describe("evaluatePullRequestWatch", () => { |
| 119 | assert.deepEqual(again.next.remarkIds, [first.id, "late"]); | 118 | assert.deepEqual(again.next.remarkIds, [first.id, "late"]); |
| 120 | }); | 119 | }); |
| 121 | 120 | ||
| 121 | it("reports edits after the watermark once and counts them toward the wake limit", () => { | ||
| 122 | const old = remark("greptile[bot]", "2026-10-02T11:00:00Z"); | ||
| 123 | const watching = watch({ | ||
| 124 | headSha: "aaaaaaaaaa", | ||
| 125 | remarkIds: [old.id], | ||
| 126 | wakes: PULL_REQUEST_WATCH_WAKE_LIMIT - 1, | ||
| 127 | }); | ||
| 128 | assert.deepEqual(evaluatePullRequestWatch(watching, detail(), [old]).changes, []); | ||
| 129 | const edited = { ...old, editedAt: "2026-10-02T12:06:00Z" }; | ||
| 130 | const report = evaluatePullRequestWatch(watching, detail(), [edited]); | ||
| 131 | assert.deepEqual(report.changes, [{ kind: "remarks", remarks: [edited] }]); | ||
| 132 | assert.equal(report.next.remarksThrough, edited.editedAt); | ||
| 133 | assert.deepEqual(report.next.remarkIds, [old.id]); | ||
| 134 | assert.isTrue(report.exhausted); | ||
| 135 | assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), [edited]).changes, []); | ||
| 136 | const late = { ...edited, id: "late" }; | ||
| 137 | assert.deepEqual(evaluatePullRequestWatch(report.next, detail(), [edited, late]).changes, [ | ||
| 138 | { kind: "remarks", remarks: [late] }, | ||
| 139 | ]); | ||
| 140 | }); | ||
| 141 | |||
| 122 | it("does not treat a failed check read as a rerun", () => { | 142 | it("does not treat a failed check read as a rerun", () => { |
| 123 | const failed = detail({ checks: [check("lint", "failure")] }); | 143 | const failed = detail({ checks: [check("lint", "failure")] }); |
| 124 | const reported = evaluatePullRequestWatch(watch(), failed, noRemarks); | 144 | const reported = evaluatePullRequestWatch(watch(), failed, noRemarks); |
apps/server/src/orchestration-v2/pullRequestWatch.ts+6-4| ... | @@ -71,18 +71,20 @@ export function evaluatePullRequestWatch( | ... | @@ -71,18 +71,20 @@ export function evaluatePullRequestWatch( |
| 71 | 71 | ||
| 72 | const own = (detail.viewer ?? detail.author?.login)?.toLowerCase(); | 72 | const own = (detail.viewer ?? detail.author?.login)?.toLowerCase(); |
| 73 | const through = Date.parse(watch.remarksThrough); | 73 | const through = Date.parse(watch.remarksThrough); |
| 74 | // An edit counts as new activity, so bots that rewrite one summary comment still wake the agent. | ||
| 75 | const activeAt = (remark: PullRequestComment) => remark.editedAt ?? remark.createdAt; | ||
| 74 | // GitHub times are per second, so remarks at the boundary time are told apart by ID. | 76 | // GitHub times are per second, so remarks at the boundary time are told apart by ID. |
| 75 | const fresh = (remarks ?? []).filter((remark) => { | 77 | const fresh = (remarks ?? []).filter((remark) => { |
| 76 | const at = Date.parse(remark.createdAt); | 78 | const at = Date.parse(activeAt(remark)); |
| 77 | return ( | 79 | return ( |
| 78 | (at > through || (at === through && !watch.remarkIds.includes(remark.id))) && | 80 | (at > through || (at === through && !watch.remarkIds.includes(remark.id))) && |
| 79 | remark.author?.login.toLowerCase() !== own | 81 | remark.author?.login.toLowerCase() !== own |
| 80 | ); | 82 | ); |
| 81 | }); | 83 | }); |
| 82 | if (fresh.length > 0) changes.push({ kind: "remarks", remarks: fresh }); | 84 | if (fresh.length > 0) changes.push({ kind: "remarks", remarks: fresh }); |
| 83 | const latest = Math.max(through, ...fresh.map((remark) => Date.parse(remark.createdAt))); | 85 | const latest = Math.max(through, ...fresh.map((remark) => Date.parse(activeAt(remark)))); |
| 84 | const atLatest = fresh.filter((remark) => Date.parse(remark.createdAt) === latest); | 86 | const atLatest = fresh.filter((remark) => Date.parse(activeAt(remark)) === latest); |
| 85 | const remarksThrough = latest === through ? watch.remarksThrough : atLatest[0]!.createdAt; | 87 | const remarksThrough = latest === through ? watch.remarksThrough : activeAt(atLatest[0]!); |
| 86 | const remarkIds = [ | 88 | const remarkIds = [ |
| 87 | ...(latest === through ? watch.remarkIds : []), | 89 | ...(latest === through ? watch.remarkIds : []), |
| 88 | ...atLatest.map((remark) => remark.id), | 90 | ...atLatest.map((remark) => remark.id), |
apps/server/src/pullRequest/GitHubPullRequestCli.ts+3| ... | @@ -2338,6 +2338,7 @@ export const make = Effect.gen(function* () { | ... | @@ -2338,6 +2338,7 @@ export const make = Effect.gen(function* () { |
| 2338 | let reviewers: ReadonlyArray<PullRequestActor> = []; | 2338 | let reviewers: ReadonlyArray<PullRequestActor> = []; |
| 2339 | let reactions: GitHubReviewThreadPage["reactions"] = []; | 2339 | let reactions: GitHubReviewThreadPage["reactions"] = []; |
| 2340 | const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>(); | 2340 | const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>(); |
| 2341 | const editedAtById = new Map<string, string>(); | ||
| 2341 | let commits: GitHubReviewThreadPage["commits"] = []; | 2342 | let commits: GitHubReviewThreadPage["commits"] = []; |
| 2342 | let viewer: GitHubReviewThreadPage["viewer"] = { canUpdate: true, didAuthor: false }; | 2343 | let viewer: GitHubReviewThreadPage["viewer"] = { canUpdate: true, didAuthor: false }; |
| 2343 | const dismissalsByReviewId = new Map<string, string>(); | 2344 | const dismissalsByReviewId = new Map<string, string>(); |
| ... | @@ -2356,6 +2357,7 @@ export const make = Effect.gen(function* () { | ... | @@ -2356,6 +2357,7 @@ export const make = Effect.gen(function* () { |
| 2356 | reviewers = read.reviewers; | 2357 | reviewers = read.reviewers; |
| 2357 | reactions = read.reactions; | 2358 | reactions = read.reactions; |
| 2358 | for (const [id, entry] of read.reactionsById) reactionsById.set(id, entry); | 2359 | for (const [id, entry] of read.reactionsById) reactionsById.set(id, entry); |
| 2360 | for (const [id, editedAt] of read.editedAtById) editedAtById.set(id, editedAt); | ||
| 2359 | commits = read.commits; | 2361 | commits = read.commits; |
| 2360 | viewer = read.viewer; | 2362 | viewer = read.viewer; |
| 2361 | for (const [id, message] of read.dismissalsByReviewId) | 2363 | for (const [id, message] of read.dismissalsByReviewId) |
| ... | @@ -2412,6 +2414,7 @@ export const make = Effect.gen(function* () { | ... | @@ -2412,6 +2414,7 @@ export const make = Effect.gen(function* () { |
| 2412 | reviewThreadsTruncated: cursor !== null, | 2414 | reviewThreadsTruncated: cursor !== null, |
| 2413 | reactions, | 2415 | reactions, |
| 2414 | reactionsById, | 2416 | reactionsById, |
| 2417 | editedAtById, | ||
| 2415 | reviewers, | 2418 | reviewers, |
| 2416 | avatarsByLogin, | 2419 | avatarsByLogin, |
| 2417 | botLogins, | 2420 | botLogins, |
apps/server/src/pullRequest/GitHubPullRequestProvider.test.ts+3| ... | @@ -885,6 +885,7 @@ describe("getChangeRequest commits", () => { | ... | @@ -885,6 +885,7 @@ describe("getChangeRequest commits", () => { |
| 885 | reviewThreadsTruncated: false, | 885 | reviewThreadsTruncated: false, |
| 886 | reactions: [], | 886 | reactions: [], |
| 887 | reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(), | 887 | reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(), |
| 888 | editedAtById: new Map<string, string>(), | ||
| 888 | reviewers: [], | 889 | reviewers: [], |
| 889 | avatarsByLogin: new Map<string, string>(), | 890 | avatarsByLogin: new Map<string, string>(), |
| 890 | botLogins: new Set<string>(), | 891 | botLogins: new Set<string>(), |
| ... | @@ -971,6 +972,7 @@ describe("getChangeRequestActivity dismissed reviews", () => { | ... | @@ -971,6 +972,7 @@ describe("getChangeRequestActivity dismissed reviews", () => { |
| 971 | reviewThreadsTruncated: false, | 972 | reviewThreadsTruncated: false, |
| 972 | reactions: [], | 973 | reactions: [], |
| 973 | reactionsById: new Map(), | 974 | reactionsById: new Map(), |
| 975 | editedAtById: new Map([["PRR_1", "2026-07-04T00:00:00Z"]]), | ||
| 974 | reviewers: [], | 976 | reviewers: [], |
| 975 | avatarsByLogin: new Map(), | 977 | avatarsByLogin: new Map(), |
| 976 | botLogins: new Set(["macroscopeapp"]), | 978 | botLogins: new Set(["macroscopeapp"]), |
| ... | @@ -1002,6 +1004,7 @@ describe("getChangeRequestActivity dismissed reviews", () => { | ... | @@ -1002,6 +1004,7 @@ describe("getChangeRequestActivity dismissed reviews", () => { |
| 1002 | Effect.map((activity) => { | 1004 | Effect.map((activity) => { |
| 1003 | expect(activity.comments[0]?.body).toBe("Dismissing prior approval to re-evaluate 9b66581"); | 1005 | expect(activity.comments[0]?.body).toBe("Dismissing prior approval to re-evaluate 9b66581"); |
| 1004 | expect(activity.comments[0]?.author?.isBot).toBe(true); | 1006 | expect(activity.comments[0]?.author?.isBot).toBe(true); |
| 1007 | expect(activity.comments[0]?.editedAt).toBe("2026-07-04T00:00:00Z"); | ||
| 1005 | }), | 1008 | }), |
| 1006 | Effect.provide(layerFor("<!-- Macroscope (Approvability) review body marker -->")), | 1009 | Effect.provide(layerFor("<!-- Macroscope (Approvability) review body marker -->")), |
| 1007 | ), | 1010 | ), |
apps/server/src/pullRequest/GitHubPullRequestProvider.ts+2| ... | @@ -409,6 +409,7 @@ export const make = Effect.gen(function* () { | ... | @@ -409,6 +409,7 @@ export const make = Effect.gen(function* () { |
| 409 | dismissalsByReviewId: new Map<string, string>(), | 409 | dismissalsByReviewId: new Map<string, string>(), |
| 410 | reactions: [], | 410 | reactions: [], |
| 411 | reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(), | 411 | reactionsById: new Map<string, ReadonlyArray<PullRequestReaction>>(), |
| 412 | editedAtById: new Map<string, string>(), | ||
| 412 | reviewThreads: [], | 413 | reviewThreads: [], |
| 413 | commentCount: 0, | 414 | commentCount: 0, |
| 414 | truncated: true, | 415 | truncated: true, |
| ... | @@ -475,6 +476,7 @@ export const make = Effect.gen(function* () { | ... | @@ -475,6 +476,7 @@ export const make = Effect.gen(function* () { |
| 475 | // A comment out of `gh pr view --json` carries none of its own: that read | 476 | // A comment out of `gh pr view --json` carries none of its own: that read |
| 476 | // reports no reaction at all, so they arrive from the GraphQL page by node id. | 477 | // reports no reaction at all, so they arrive from the GraphQL page by node id. |
| 477 | reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [], | 478 | reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [], |
| 479 | editedAt: reviewThreads.editedAtById.get(comment.id) ?? comment.editedAt ?? null, | ||
| 478 | })) | 480 | })) |
| 479 | .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)), | 481 | .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)), |
| 480 | // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two | 482 | // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two |
apps/server/src/pullRequest/gitHubPullRequestJson.test.ts+33-1| ... | @@ -1105,7 +1105,13 @@ describe("review thread decoding", () => { | ... | @@ -1105,7 +1105,13 @@ describe("review thread decoding", () => { |
| 1105 | path: "src/a.ts", | 1105 | path: "src/a.ts", |
| 1106 | line: 42, | 1106 | line: 42, |
| 1107 | diffSide: "LEFT", | 1107 | diffSide: "LEFT", |
| 1108 | comments: { totalCount: 2, nodes: [comment("c1", "first"), comment("c2", "second")] }, | 1108 | comments: { |
| 1109 | totalCount: 2, | ||
| 1110 | nodes: [ | ||
| 1111 | { ...comment("c1", "first"), lastEditedAt: "2026-07-02T00:00:00Z" }, | ||
| 1112 | comment("c2", "second"), | ||
| 1113 | ], | ||
| 1114 | }, | ||
| 1109 | }, | 1115 | }, |
| 1110 | ]), | 1116 | ]), |
| 1111 | ), | 1117 | ), |
| ... | @@ -1124,6 +1130,7 @@ describe("review thread decoding", () => { | ... | @@ -1124,6 +1130,7 @@ describe("review thread decoding", () => { |
| 1124 | author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" }, | 1130 | author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" }, |
| 1125 | body: "first", | 1131 | body: "first", |
| 1126 | createdAt: "2026-07-01T00:00:00Z", | 1132 | createdAt: "2026-07-01T00:00:00Z", |
| 1133 | editedAt: "2026-07-02T00:00:00Z", | ||
| 1127 | url: "https://github.com/acme/web/pull/1#discussion_rc1", | 1134 | url: "https://github.com/acme/web/pull/1#discussion_rc1", |
| 1128 | reactions: [], | 1135 | reactions: [], |
| 1129 | }, | 1136 | }, |
| ... | @@ -1132,12 +1139,16 @@ describe("review thread decoding", () => { | ... | @@ -1132,12 +1139,16 @@ describe("review thread decoding", () => { |
| 1132 | author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" }, | 1139 | author: { login: "bilal", name: null, avatarUrl: "https://avatars/b.png" }, |
| 1133 | body: "second", | 1140 | body: "second", |
| 1134 | createdAt: "2026-07-01T00:00:00Z", | 1141 | createdAt: "2026-07-01T00:00:00Z", |
| 1142 | editedAt: null, | ||
| 1135 | url: "https://github.com/acme/web/pull/1#discussion_rc2", | 1143 | url: "https://github.com/acme/web/pull/1#discussion_rc2", |
| 1136 | reactions: [], | 1144 | reactions: [], |
| 1137 | }, | 1145 | }, |
| 1138 | ], | 1146 | ], |
| 1139 | }, | 1147 | }, |
| 1140 | ]); | 1148 | ]); |
| 1149 | expect( | ||
| 1150 | reviewThreadConversation(reviewThreads.threads.map((entry) => entry.thread))[0]?.editedAt, | ||
| 1151 | ).toBe("2026-07-02T00:00:00Z"); | ||
| 1141 | }); | 1152 | }); |
| 1142 | 1153 | ||
| 1143 | it("leaves an outdated thread without a line rather than pinning it to a stale one", () => { | 1154 | it("leaves an outdated thread without a line rather than pinning it to a stale one", () => { |
| ... | @@ -1186,6 +1197,27 @@ describe("review thread decoding", () => { | ... | @@ -1186,6 +1197,27 @@ describe("review thread decoding", () => { |
| 1186 | expect(threads).toHaveLength(1); | 1197 | expect(threads).toHaveLength(1); |
| 1187 | }); | 1198 | }); |
| 1188 | 1199 | ||
| 1200 | it("reads edit times for issue comments and reviews without using reaction updates", () => { | ||
| 1201 | const editedAt = "2026-10-02T12:06:00Z"; | ||
| 1202 | const result = expectSuccess( | ||
| 1203 | decodeReviewThreadsJson( | ||
| 1204 | threadsJson([], { | ||
| 1205 | comments: { | ||
| 1206 | nodes: [ | ||
| 1207 | { id: "edited", lastEditedAt: editedAt }, | ||
| 1208 | { id: "reaction-only", lastEditedAt: null, updatedAt: editedAt }, | ||
| 1209 | ], | ||
| 1210 | }, | ||
| 1211 | reviews: { nodes: [{ id: "review", lastEditedAt: editedAt }] }, | ||
| 1212 | }), | ||
| 1213 | ), | ||
| 1214 | ); | ||
| 1215 | expect([...result.editedAtById]).toEqual([ | ||
| 1216 | ["edited", editedAt], | ||
| 1217 | ["review", editedAt], | ||
| 1218 | ]); | ||
| 1219 | }); | ||
| 1220 | |||
| 1189 | it("puts an issue comment's and a review's reactions in reactionsById, and the pull request's own in reactions", () => { | 1221 | it("puts an issue comment's and a review's reactions in reactionsById, and the pull request's own in reactions", () => { |
| 1190 | const result = expectSuccess( | 1222 | const result = expectSuccess( |
| 1191 | decodeReviewThreadsJson( | 1223 | decodeReviewThreadsJson( |
apps/server/src/pullRequest/gitHubPullRequestJson.ts+19-4| ... | @@ -362,6 +362,7 @@ function toReactions( | ... | @@ -362,6 +362,7 @@ function toReactions( |
| 362 | 362 | ||
| 363 | const RawCommentSchema = Schema.Struct({ | 363 | const RawCommentSchema = Schema.Struct({ |
| 364 | id: Schema.String, | 364 | id: Schema.String, |
| 365 | lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 365 | author: Schema.optional(Schema.NullOr(RawActorSchema)), | 366 | author: Schema.optional(Schema.NullOr(RawActorSchema)), |
| 366 | body: Schema.optional(Schema.String), | 367 | body: Schema.optional(Schema.String), |
| 367 | createdAt: Schema.String, | 368 | createdAt: Schema.String, |
| ... | @@ -372,6 +373,7 @@ const RawCommentSchema = Schema.Struct({ | ... | @@ -372,6 +373,7 @@ const RawCommentSchema = Schema.Struct({ |
| 372 | 373 | ||
| 373 | const RawReviewSchema = Schema.Struct({ | 374 | const RawReviewSchema = Schema.Struct({ |
| 374 | id: Schema.String, | 375 | id: Schema.String, |
| 376 | lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 375 | author: Schema.optional(Schema.NullOr(RawActorSchema)), | 377 | author: Schema.optional(Schema.NullOr(RawActorSchema)), |
| 376 | body: Schema.optional(Schema.String), | 378 | body: Schema.optional(Schema.String), |
| 377 | state: Schema.optional(Schema.NullOr(Schema.String)), | 379 | state: Schema.optional(Schema.NullOr(Schema.String)), |
| ... | @@ -490,6 +492,7 @@ const RawReviewThreadsSchema = Schema.Struct({ | ... | @@ -490,6 +492,7 @@ const RawReviewThreadsSchema = Schema.Struct({ |
| 490 | Schema.Struct({ | 492 | Schema.Struct({ |
| 491 | id: Schema.optional(Schema.NullOr(Schema.String)), | 493 | id: Schema.optional(Schema.NullOr(Schema.String)), |
| 492 | author: Schema.optional(Schema.NullOr(RawActorSchema)), | 494 | author: Schema.optional(Schema.NullOr(RawActorSchema)), |
| 495 | lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 493 | reactionGroups: RawReactionGroupsSchema, | 496 | reactionGroups: RawReactionGroupsSchema, |
| 494 | }), | 497 | }), |
| 495 | ), | 498 | ), |
| ... | @@ -507,6 +510,7 @@ const RawReviewThreadsSchema = Schema.Struct({ | ... | @@ -507,6 +510,7 @@ const RawReviewThreadsSchema = Schema.Struct({ |
| 507 | Schema.Struct({ | 510 | Schema.Struct({ |
| 508 | id: Schema.optional(Schema.NullOr(Schema.String)), | 511 | id: Schema.optional(Schema.NullOr(Schema.String)), |
| 509 | author: Schema.optional(Schema.NullOr(RawActorSchema)), | 512 | author: Schema.optional(Schema.NullOr(RawActorSchema)), |
| 513 | lastEditedAt: Schema.optional(Schema.NullOr(Schema.String)), | ||
| 510 | reactionGroups: RawReactionGroupsSchema, | 514 | reactionGroups: RawReactionGroupsSchema, |
| 511 | }), | 515 | }), |
| 512 | ), | 516 | ), |
| ... | @@ -885,7 +889,7 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin | ... | @@ -885,7 +889,7 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin |
| 885 | comments(first: 10) { | 889 | comments(first: 10) { |
| 886 | totalCount | 890 | totalCount |
| 887 | pageInfo { hasNextPage endCursor } | 891 | pageInfo { hasNextPage endCursor } |
| 888 | nodes { id author { __typename login avatarUrl } body createdAt url ${REACTION_GROUPS_FIELDS} } | 892 | nodes { id author { __typename login avatarUrl } body createdAt lastEditedAt url ${REACTION_GROUPS_FIELDS} } |
| 889 | } | 893 | } |
| 890 | } | 894 | } |
| 891 | } | 895 | } |
| ... | @@ -894,9 +898,9 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin | ... | @@ -894,9 +898,9 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin |
| 894 | author { __typename login avatarUrl } | 898 | author { __typename login avatarUrl } |
| 895 | ${REACTION_GROUPS_FIELDS} | 899 | ${REACTION_GROUPS_FIELDS} |
| 896 | comments(first: ${GRAPHQL_PAGE_SIZE}) { | 900 | comments(first: ${GRAPHQL_PAGE_SIZE}) { |
| 897 | nodes { id author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} } | 901 | nodes { id lastEditedAt author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} } |
| 898 | } | 902 | } |
| 899 | reviews(first: ${GRAPHQL_PAGE_SIZE}) { nodes { id author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} } } | 903 | reviews(first: ${GRAPHQL_PAGE_SIZE}) { nodes { id lastEditedAt author { __typename login avatarUrl } ${REACTION_GROUPS_FIELDS} } } |
| 900 | reviewRequests(first: 50) { | 904 | reviewRequests(first: 50) { |
| 901 | nodes { | 905 | nodes { |
| 902 | requestedReviewer { | 906 | requestedReviewer { |
| ... | @@ -942,7 +946,7 @@ export const REVIEW_THREAD_COMMENTS_GRAPHQL_QUERY = `query($owner: String!, $nam | ... | @@ -942,7 +946,7 @@ export const REVIEW_THREAD_COMMENTS_GRAPHQL_QUERY = `query($owner: String!, $nam |
| 942 | pullRequest { id } | 946 | pullRequest { id } |
| 943 | comments(first: ${GRAPHQL_PAGE_SIZE}, after: $cursor) { | 947 | comments(first: ${GRAPHQL_PAGE_SIZE}, after: $cursor) { |
| 944 | pageInfo { hasNextPage endCursor } | 948 | pageInfo { hasNextPage endCursor } |
| 945 | nodes { id author { __typename login avatarUrl } body createdAt url ${REACTION_GROUPS_FIELDS} } | 949 | nodes { id author { __typename login avatarUrl } body createdAt lastEditedAt url ${REACTION_GROUPS_FIELDS} } |
| 946 | } | 950 | } |
| 947 | } | 951 | } |
| 948 | } | 952 | } |
| ... | @@ -1542,6 +1546,7 @@ function toComments(raw: { | ... | @@ -1542,6 +1546,7 @@ function toComments(raw: { |
| 1542 | author: toActor(comment.author), | 1546 | author: toActor(comment.author), |
| 1543 | body: comment.body ?? "", | 1547 | body: comment.body ?? "", |
| 1544 | createdAt: comment.createdAt, | 1548 | createdAt: comment.createdAt, |
| 1549 | editedAt: comment.lastEditedAt ?? null, | ||
| 1545 | url: trimmed(comment.url), | 1550 | url: trimmed(comment.url), |
| 1546 | path: null, | 1551 | path: null, |
| 1547 | reviewState: null, | 1552 | reviewState: null, |
| ... | @@ -1566,6 +1571,7 @@ function toComments(raw: { | ... | @@ -1566,6 +1571,7 @@ function toComments(raw: { |
| 1566 | author: toActor(review.author), | 1571 | author: toActor(review.author), |
| 1567 | body: review.body ?? "", | 1572 | body: review.body ?? "", |
| 1568 | createdAt: submittedAt, | 1573 | createdAt: submittedAt, |
| 1574 | editedAt: review.lastEditedAt ?? null, | ||
| 1569 | url: trimmed(review.url), | 1575 | url: trimmed(review.url), |
| 1570 | path: null, | 1576 | path: null, |
| 1571 | reviewState, | 1577 | reviewState, |
| ... | @@ -2098,6 +2104,7 @@ export interface GitHubReviewThreadComments { | ... | @@ -2098,6 +2104,7 @@ export interface GitHubReviewThreadComments { |
| 2098 | readonly reactions: ReadonlyArray<PullRequestReaction>; | 2104 | readonly reactions: ReadonlyArray<PullRequestReaction>; |
| 2099 | /** Reactions by node id, for the comments and reviews the `gh` JSON read carries no reaction on. */ | 2105 | /** Reactions by node id, for the comments and reviews the `gh` JSON read carries no reaction on. */ |
| 2100 | readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>; | 2106 | readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>; |
| 2107 | readonly editedAtById: ReadonlyMap<string, string>; | ||
| 2101 | /** | 2108 | /** |
| 2102 | * Everyone on the review: those still asked and those who have already answered. Whoever has | 2109 | * Everyone on the review: those still asked and those who have already answered. Whoever has |
| 2103 | * reviewed is no longer an outstanding request, so asking only for requests reports nobody on | 2110 | * reviewed is no longer an outstanding request, so asking only for requests reports nobody on |
| ... | @@ -2147,6 +2154,7 @@ export interface GitHubReviewThreadPage { | ... | @@ -2147,6 +2154,7 @@ export interface GitHubReviewThreadPage { |
| 2147 | * for without any. Only ids with a reaction are here; the rest carry none. | 2154 | * for without any. Only ids with a reaction are here; the rest carry none. |
| 2148 | */ | 2155 | */ |
| 2149 | readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>; | 2156 | readonly reactionsById: ReadonlyMap<string, ReadonlyArray<PullRequestReaction>>; |
| 2157 | readonly editedAtById: ReadonlyMap<string, string>; | ||
| 2150 | readonly reviewers: ReadonlyArray<PullRequestActor>; | 2158 | readonly reviewers: ReadonlyArray<PullRequestActor>; |
| 2151 | readonly avatarsByLogin: ReadonlyMap<string, string>; | 2159 | readonly avatarsByLogin: ReadonlyMap<string, string>; |
| 2152 | readonly botLogins: ReadonlySet<string>; | 2160 | readonly botLogins: ReadonlySet<string>; |
| ... | @@ -2177,6 +2185,7 @@ export function reviewThreadConversation( | ... | @@ -2177,6 +2185,7 @@ export function reviewThreadConversation( |
| 2177 | author: comment.author, | 2185 | author: comment.author, |
| 2178 | body: comment.body, | 2186 | body: comment.body, |
| 2179 | createdAt: comment.createdAt, | 2187 | createdAt: comment.createdAt, |
| 2188 | editedAt: comment.editedAt ?? null, | ||
| 2180 | url: comment.url, | 2189 | url: comment.url, |
| 2181 | path: thread.path, | 2190 | path: thread.path, |
| 2182 | reviewState: null, | 2191 | reviewState: null, |
| ... | @@ -2276,6 +2285,7 @@ export function decodeReviewThreadsJson( | ... | @@ -2276,6 +2285,7 @@ export function decodeReviewThreadsJson( |
| 2276 | author: toActor(comment.author), | 2285 | author: toActor(comment.author), |
| 2277 | body: comment.body ?? "", | 2286 | body: comment.body ?? "", |
| 2278 | createdAt: comment.createdAt, | 2287 | createdAt: comment.createdAt, |
| 2288 | editedAt: comment.lastEditedAt ?? null, | ||
| 2279 | url: trimmed(comment.url), | 2289 | url: trimmed(comment.url), |
| 2280 | reactions: toReactions(comment.reactionGroups, viewer), | 2290 | reactions: toReactions(comment.reactionGroups, viewer), |
| 2281 | })), | 2291 | })), |
| ... | @@ -2342,12 +2352,15 @@ export function decodeReviewThreadsJson( | ... | @@ -2342,12 +2352,15 @@ export function decodeReviewThreadsJson( |
| 2342 | }); | 2352 | }); |
| 2343 | } | 2353 | } |
| 2344 | const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>(); | 2354 | const reactionsById = new Map<string, ReadonlyArray<PullRequestReaction>>(); |
| 2355 | const editedAtById = new Map<string, string>(); | ||
| 2345 | for (const node of [ | 2356 | for (const node of [ |
| 2346 | ...(pullRequest.comments?.nodes ?? []), | 2357 | ...(pullRequest.comments?.nodes ?? []), |
| 2347 | ...(pullRequest.reviews?.nodes ?? []), | 2358 | ...(pullRequest.reviews?.nodes ?? []), |
| 2348 | ]) { | 2359 | ]) { |
| 2349 | const id = trimmed(node.id); | 2360 | const id = trimmed(node.id); |
| 2350 | if (id === null) continue; | 2361 | if (id === null) continue; |
| 2362 | const editedAt = trimmed(node.lastEditedAt); | ||
| 2363 | if (editedAt !== null) editedAtById.set(id, editedAt); | ||
| 2351 | const reactions = toReactions(node.reactionGroups, viewer); | 2364 | const reactions = toReactions(node.reactionGroups, viewer); |
| 2352 | if (reactions.length > 0) reactionsById.set(id, reactions); | 2365 | if (reactions.length > 0) reactionsById.set(id, reactions); |
| 2353 | } | 2366 | } |
| ... | @@ -2356,6 +2369,7 @@ export function decodeReviewThreadsJson( | ... | @@ -2356,6 +2369,7 @@ export function decodeReviewThreadsJson( |
| 2356 | nextCursor: nextCursorOf(threads.pageInfo), | 2369 | nextCursor: nextCursorOf(threads.pageInfo), |
| 2357 | reactions: toReactions(pullRequest.reactionGroups, viewer), | 2370 | reactions: toReactions(pullRequest.reactionGroups, viewer), |
| 2358 | reactionsById, | 2371 | reactionsById, |
| 2372 | editedAtById, | ||
| 2359 | reviewers: [...reviewers.values()], | 2373 | reviewers: [...reviewers.values()], |
| 2360 | avatarsByLogin, | 2374 | avatarsByLogin, |
| 2361 | botLogins, | 2375 | botLogins, |
| ... | @@ -2392,6 +2406,7 @@ export function decodeReviewThreadCommentsJson(raw: string): Result.Result< | ... | @@ -2392,6 +2406,7 @@ export function decodeReviewThreadCommentsJson(raw: string): Result.Result< |
| 2392 | author: toActor(comment.author), | 2406 | author: toActor(comment.author), |
| 2393 | body: comment.body ?? "", | 2407 | body: comment.body ?? "", |
| 2394 | createdAt: comment.createdAt, | 2408 | createdAt: comment.createdAt, |
| 2409 | editedAt: comment.lastEditedAt ?? null, | ||
| 2395 | url: trimmed(comment.url), | 2410 | url: trimmed(comment.url), |
| 2396 | reactions: toReactions(comment.reactionGroups, viewer), | 2411 | reactions: toReactions(comment.reactionGroups, viewer), |
| 2397 | })), | 2412 | })), |
packages/contracts/src/pullRequest.ts+2| ... | @@ -203,6 +203,7 @@ export const PullRequestComment = Schema.Struct({ | ... | @@ -203,6 +203,7 @@ export const PullRequestComment = Schema.Struct({ |
| 203 | author: Schema.NullOr(PullRequestActor), | 203 | author: Schema.NullOr(PullRequestActor), |
| 204 | body: Schema.String, | 204 | body: Schema.String, |
| 205 | createdAt: IsoDateTime, | 205 | createdAt: IsoDateTime, |
| 206 | editedAt: Schema.optional(Schema.NullOr(IsoDateTime)), | ||
| 206 | url: Schema.NullOr(Schema.String), | 207 | url: Schema.NullOr(Schema.String), |
| 207 | path: Schema.NullOr(Schema.String), | 208 | path: Schema.NullOr(Schema.String), |
| 208 | reviewState: Schema.NullOr(Schema.String), | 209 | reviewState: Schema.NullOr(Schema.String), |
| ... | @@ -228,6 +229,7 @@ export const PullRequestThreadComment = Schema.Struct({ | ... | @@ -228,6 +229,7 @@ export const PullRequestThreadComment = Schema.Struct({ |
| 228 | author: Schema.NullOr(PullRequestActor), | 229 | author: Schema.NullOr(PullRequestActor), |
| 229 | body: Schema.String, | 230 | body: Schema.String, |
| 230 | createdAt: IsoDateTime, | 231 | createdAt: IsoDateTime, |
| 232 | editedAt: Schema.optional(Schema.NullOr(IsoDateTime)), | ||
| 231 | url: Schema.NullOr(Schema.String), | 233 | url: Schema.NullOr(Schema.String), |
| 232 | reactions: Schema.optional(Schema.Array(PullRequestReaction)), | 234 | reactions: Schema.optional(Schema.Array(PullRequestReaction)), |
| 233 | }); | 235 | }); |