| ... | ... | @@ -46,9 +46,9 @@ const RawActorSchema = Schema.Struct({ |
| 46 | 46 | __typename: Schema.optional(Schema.String), |
| 47 | 47 | is_bot: Schema.optional(Schema.Boolean), |
| 48 | 48 | /** |
| 49 | | * Optional because a review can be requested from a team or a mannequin, which the query has |
| 50 | | * no fragment for and GraphQL answers with an empty object. A reviewer with no login names |
| 51 | | * nobody to show, and must not fail the response the conversation travels in. |
| 49 | * Optional because a requested team goes by its slug instead, and a mannequin, which no query |
| 50 | * has a fragment for, arrives as an empty object. A reviewer with neither names nobody to show, |
| 51 | * and must not fail the response the conversation travels in. |
| 52 | 52 | */ |
| 53 | 53 | login: Schema.optional(Schema.String), |
| 54 | 54 | /** The node id, which is how a listing's authors are resolved to avatars in one request. */ |
| ... | ... | @@ -58,6 +58,16 @@ const RawActorSchema = Schema.Struct({ |
| 58 | 58 | avatarUrl: Schema.optional(Schema.NullOr(Schema.String)), |
| 59 | 59 | }); |
| 60 | 60 | |
| 61 | /** |
| 62 | * A team answers with a slug where a user answers with a login. `combinedSlug` is the team's |
| 63 | * `org/slug`, which is what a reviewer row shows for it. |
| 64 | */ |
| 65 | const RawRequestedReviewerSchema = Schema.Struct({ |
| 66 | ...RawActorSchema.fields, |
| 67 | slug: Schema.optional(Schema.NullOr(Schema.String)), |
| 68 | combinedSlug: Schema.optional(Schema.NullOr(Schema.String)), |
| 69 | }); |
| 70 | |
| 61 | 71 | const RawLabelSchema = Schema.Struct({ |
| 62 | 72 | name: Schema.String, |
| 63 | 73 | color: Schema.optional(Schema.NullOr(Schema.String)), |
| ... | ... | @@ -66,7 +76,9 @@ const RawLabelSchema = Schema.Struct({ |
| 66 | 76 | const RawReviewRequestSchema = Schema.Struct({ |
| 67 | 77 | login: Schema.optional(Schema.NullOr(Schema.String)), |
| 68 | 78 | slug: Schema.optional(Schema.NullOr(Schema.String)), |
| 79 | combinedSlug: Schema.optional(Schema.NullOr(Schema.String)), |
| 69 | 80 | name: Schema.optional(Schema.NullOr(Schema.String)), |
| 81 | avatarUrl: Schema.optional(Schema.NullOr(Schema.String)), |
| 70 | 82 | }); |
| 71 | 83 | |
| 72 | 84 | /** One reviewer's most recent review: the state is all the verdict needs, the author is for who. */ |
| ... | ... | @@ -618,8 +630,8 @@ const RawReviewThreadsSchema = Schema.Struct({ |
| 618 | 630 | Schema.Struct({ |
| 619 | 631 | nodes: Schema.Array( |
| 620 | 632 | Schema.Struct({ |
| 621 | | // Null for a team, which is a request nobody in particular owns. |
| 622 | | requestedReviewer: Schema.optional(Schema.NullOr(RawActorSchema)), |
| 633 | // Null where GitHub does not name who was asked. |
| 634 | requestedReviewer: Schema.optional(Schema.NullOr(RawRequestedReviewerSchema)), |
| 623 | 635 | }), |
| 624 | 636 | ), |
| 625 | 637 | }), |
| ... | ... | @@ -835,7 +847,7 @@ export const pullRequestCoreGraphQlQuery = (host: string) => { |
| 835 | 847 | viewerCanUpdate viewerDidAuthor viewerCanUpdateBranch |
| 836 | 848 | baseRef { compare(headRef: $headRef) { behindBy } } |
| 837 | 849 | reviewRequests(first: 100) { |
| 838 | | nodes { requestedReviewer { ... on User { login name } ... on Bot { login } ... on Team { slug name } } } |
| 850 | nodes { requestedReviewer { ... on User { login name } ... on Bot { login } ... on Team { slug combinedSlug name avatarUrl } } } |
| 839 | 851 | } |
| 840 | 852 | labels(first: 100) { nodes { name color } } |
| 841 | 853 | commits(last: 1) { |
| ... | ... | @@ -1006,8 +1018,8 @@ function pullRequestRowSelection(includeStacks: boolean, includeStats: boolean): |
| 1006 | 1018 | * |
| 1007 | 1019 | * Reviewers come from here rather than from `gh pr view --json reviewRequests` for two reasons: |
| 1008 | 1020 | * that field holds only requests still outstanding, so anyone who has already reviewed drops off |
| 1009 | | * it, and neither it nor any other `gh` JSON field carries an avatar. A reviewer can be a person |
| 1010 | | * or an app, and both are asked for by name because they are different GraphQL types. |
| 1021 | * it, and neither it nor any other `gh` JSON field carries an avatar. A reviewer can be a person, |
| 1022 | * an app, or a team, and each is asked for by name because they are different GraphQL types. |
| 1011 | 1023 | * |
| 1012 | 1024 | * `viewerCanUpdate` and `viewerDidAuthor` ride along here for the same reason: they belong to the |
| 1013 | 1025 | * pull request this query is already standing on, so what the reader may do with it arrives with |
| ... | ... | @@ -1052,6 +1064,7 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin |
| 1052 | 1064 | requestedReviewer { |
| 1053 | 1065 | ... on User { login name avatarUrl } |
| 1054 | 1066 | ... on Bot { __typename login avatarUrl } |
| 1067 | ... on Team { combinedSlug name avatarUrl } |
| 1055 | 1068 | } |
| 1056 | 1069 | } |
| 1057 | 1070 | } |
| ... | ... | @@ -1413,6 +1426,22 @@ function toActor(raw: Schema.Schema.Type<typeof RawActorSchema> | null | undefin |
| 1413 | 1426 | }; |
| 1414 | 1427 | } |
| 1415 | 1428 | |
| 1429 | /** |
| 1430 | * A requested team as a reviewer row. It goes by `org/slug`, the handle GitHub mentions it by. |
| 1431 | * No GitHub login contains a slash, so a team never shares a row or a verdict with a user. |
| 1432 | */ |
| 1433 | function toRequestedTeam( |
| 1434 | raw: |
| 1435 | | Pick<Schema.Schema.Type<typeof RawReviewRequestSchema>, "combinedSlug" | "name" | "avatarUrl"> |
| 1436 | | null |
| 1437 | | undefined, |
| 1438 | ): PullRequestActor | null { |
| 1439 | const login = trimmed(raw?.combinedSlug); |
| 1440 | return login === null |
| 1441 | ? null |
| 1442 | : { login, name: trimmed(raw?.name), avatarUrl: trimmed(raw?.avatarUrl) }; |
| 1443 | } |
| 1444 | |
| 1416 | 1445 | function toCommitActor( |
| 1417 | 1446 | raw: NonNullable<Schema.Schema.Type<typeof RawCommitSchema>["authors"]>[number], |
| 1418 | 1447 | ): PullRequestActor | null { |
| ... | ... | @@ -2302,6 +2331,8 @@ export function decodePullRequestWatchFingerprintsJson( |
| 2302 | 2331 | } |
| 2303 | 2332 | |
| 2304 | 2333 | export interface GitHubPullRequestCore extends GitHubPullRequestDetail { |
| 2334 | /** Teams asked for a review. `reviewRequestLogins` holds the people. */ |
| 2335 | readonly reviewRequestTeams: ReadonlyArray<PullRequestActor>; |
| 2305 | 2336 | readonly viewerAccess: GitHubViewerAccess & GitHubRepositoryAccess; |
| 2306 | 2337 | readonly comparison: GitHubBaseComparison | null; |
| 2307 | 2338 | readonly checksTruncated: boolean; |
| ... | ... | @@ -2316,6 +2347,9 @@ export function decodePullRequestCoreJson( |
| 2316 | 2347 | const pr = repository.pullRequest; |
| 2317 | 2348 | const contexts = pr.commits.nodes[0]?.commit.statusCheckRollup?.contexts; |
| 2318 | 2349 | return Result.succeed({ |
| 2350 | reviewRequestTeams: pr.reviewRequests.nodes.flatMap( |
| 2351 | ({ requestedReviewer }) => toRequestedTeam(requestedReviewer) ?? [], |
| 2352 | ), |
| 2319 | 2353 | ...toDetail({ |
| 2320 | 2354 | ...pr, |
| 2321 | 2355 | reviewRequests: pr.reviewRequests.nodes.flatMap(({ requestedReviewer }) => |
| ... | ... | @@ -2772,7 +2806,7 @@ export function decodeReviewThreadsJson( |
| 2772 | 2806 | ...(pullRequest.reviewRequests?.nodes ?? []).map((node) => node.requestedReviewer), |
| 2773 | 2807 | ...(pullRequest.latestReviews?.nodes ?? []).map((node) => node.author), |
| 2774 | 2808 | ]) { |
| 2775 | | const actor = toActor(raw); |
| 2809 | const actor = toActor(raw) ?? toRequestedTeam(raw); |
| 2776 | 2810 | // Keyed by login, so someone who was asked and then answered appears once. |
| 2777 | 2811 | if (actor !== null && !reviewers.has(actor.login)) reviewers.set(actor.login, actor); |
| 2778 | 2812 | } |
| ... | ... | @@ -2932,7 +2966,7 @@ export const REVIEWER_CANDIDATES_GRAPHQL_QUERY = `query($owner: String!, $name: |
| 2932 | 2966 | nodes { |
| 2933 | 2967 | requestedReviewer { |
| 2934 | 2968 | ... on User { login name avatarUrl } |
| 2935 | | ... on Team { slug name avatarUrl } |
| 2969 | ... on Team { slug combinedSlug name avatarUrl } |
| 2936 | 2970 | ... on Bot { login avatarUrl } |
| 2937 | 2971 | } |
| 2938 | 2972 | } |
| ... | ... | @@ -2941,12 +2975,6 @@ export const REVIEWER_CANDIDATES_GRAPHQL_QUERY = `query($owner: String!, $name: |
| 2941 | 2975 | } |
| 2942 | 2976 | }`; |
| 2943 | 2977 | |
| 2944 | | /** A team answers with a slug where a user answers with a login, and nothing else differs. */ |
| 2945 | | const RawRequestedReviewerSchema = Schema.Struct({ |
| 2946 | | ...RawActorSchema.fields, |
| 2947 | | slug: Schema.optional(Schema.NullOr(Schema.String)), |
| 2948 | | }); |
| 2949 | | |
| 2950 | 2978 | const RawReviewerCandidatesSchema = Schema.Struct({ |
| 2951 | 2979 | data: Schema.Struct({ |
| 2952 | 2980 | repository: Schema.Struct({ |
| ... | ... | @@ -3004,7 +3032,8 @@ export function decodeReviewerCandidatesJson( |
| 3004 | 3032 | candidates.set(`${slug === null ? "user" : "team"} ${id}`, { |
| 3005 | 3033 | id, |
| 3006 | 3034 | kind: slug === null ? "user" : "team", |
| 3007 | | login: id, |
| 3035 | // The same `org/slug` the reviewer row shows, so taking the request back finds that row. |
| 3036 | login: slug === null ? id : (trimmed(raw?.combinedSlug) ?? id), |
| 3008 | 3037 | name: trimmed(raw?.name), |
| 3009 | 3038 | avatarUrl: trimmed(raw?.avatarUrl), |
| 3010 | 3039 | isRequested: true, |