# The review conversation: threads, their comments, and what the people on the review said. # A reaction group as every reactable node reports it. `reactors` is bounded rather than paged: a # hover says who reacted, and a hundred and forty names is a count, not a sentence. fragment Reactions on Reactable { reactionGroups { content viewerHasReacted reactors(first: 10) { totalCount nodes { ... on User { login } ... on Bot { login } ... on Organization { login } ... on Mannequin { login } } } } } fragment ThreadComment on PullRequestReviewComment { id author { __typename login avatarUrl } body createdAt lastEditedAt url isMinimized pullRequestReview { url id state } ...Reactions } # One page of review threads. Only ten comments ride with each thread. A hundred threads times a # hundred comments made GitHub reserve 10,000 nested rows and charge 104 points; unfinished threads # are paged from their own cursor by `ReviewThreadComments`. fragment ReviewThreadPage on PullRequestReviewThreadConnection { totalCount pageInfo { hasNextPage endCursor } nodes { id isResolved isOutdated path # Null once the thread's line has left the diff, which `isOutdated` reports. line diffSide startLine startDiffSide comments(first: 10) { totalCount pageInfo { hasNextPage endCursor } nodes { ...ThreadComment } } } } # The first page of review threads with their comments, and the people on the review. Later pages # come from `ReviewThreadPages`, so a pull request with more threads than one page holds is walked # rather than cut off. # # `viewer` rides along: GitHub names who reacted but never says whether that is the reader, so the # comparison is made locally rather than paid for with a request of its own. # # Reviewers come from here because `reviewRequests` holds only requests still outstanding, so # anyone who has already reviewed drops off it; `latestReviews` adds them back. A reviewer can be a # person or an app, and both are asked for by name because they are different GraphQL types. # # `viewerCanUpdate` and `viewerDidAuthor` belong to the pull request this query is already # standing on, so what the reader may do with it arrives with the conversation. query ReviewThreads($owner: String!, $name: String!, $number: Int!, $requestedReviewers: Int!) { viewer { login } repository(owner: $owner, name: $name) { pullRequest(number: $number) { reviewThreads(first: 100) { ...ReviewThreadPage } viewerCanUpdate viewerDidAuthor author { __typename login avatarUrl } ...Reactions comments(first: 100) { nodes { id lastEditedAt author { __typename login avatarUrl } ...Reactions } } # For their reactions and edit times: the words and the verdict arrive with the activity read. reviews(first: 100) { nodes { id lastEditedAt author { __typename login avatarUrl } ...Reactions } } reviewRequests(first: $requestedReviewers) { nodes { # Null for nobody. A team goes by its `org/slug`; a mannequin answers with neither and # names nobody to show. A team's name and avatar are aliased apart from a person's, # since their nullability differs. requestedReviewer { ... on User { login name avatarUrl } ... on Bot { login avatarUrl } ... on Team { combinedSlug teamName: name teamAvatarUrl: avatarUrl } } } } latestReviews(first: 50) { nodes { state isMinimized author { __typename login avatarUrl } } } reviewDismissals: timelineItems(itemTypes: [REVIEW_DISMISSED_EVENT], first: 100) { pageInfo { hasNextPage endCursor } nodes { ... on ReviewDismissedEvent { dismissalMessage review { id } } } } # `last` rather than `first`: a pull request with more than a hundred commits keeps the # newest ones, which is what a reader scoping a diff wants. commits(last: 100) { nodes { commit { oid messageHeadline committedDate authors(first: 3) { nodes { name avatarUrl user { login } } } } } } } } } # The review threads past the first page. Everything else `ReviewThreads` asks for describes the # whole pull request and arrived with that page, so later pages carry the threads alone. query ReviewThreadPages($owner: String!, $name: String!, $number: Int!, $after: String!) { viewer { login } repository(owner: $owner, name: $name) { pullRequest(number: $number) { reviewThreads(first: 100, after: $after) { ...ReviewThreadPage } } } } # The rest of one thread's conversation. GraphQL pages a connection nested inside another only # from the inner node itself, so a thread longer than a page is followed on its own: a request # GitHub makes necessary, and one no ordinary pull request ever provokes. query ReviewThreadComments( $owner: String! $name: String! $number: Int! $threadId: ID! $after: String ) { viewer { login } repository(owner: $owner, name: $name) { pullRequest(number: $number) { id } } node(id: $threadId) { ... on PullRequestReviewThread { pullRequest { id } comments(first: 100, after: $after) { pageInfo { hasNextPage endCursor } nodes { ...ThreadComment } } } } } # The dismissal events past the page the thread read carries. A pull request rarely has any: this # is followed only while the embedded page reports more. query ReviewDismissals($owner: String!, $name: String!, $number: Int!, $after: String) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { timelineItems(itemTypes: [REVIEW_DISMISSED_EVENT], first: 100, after: $after) { pageInfo { hasNextPage endCursor } nodes { ... on ReviewDismissedEvent { dismissalMessage review { id } } } } } } } # Personal review drafts, private to the authenticated reviewer. query PendingReview($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { id reviews(first: 100, states: PENDING) { nodes { id fullDatabaseId body viewerDidAuthor } } } } } query PendingReviewPositions($owner: String!, $name: String!, $number: Int!, $cursor: String) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { reviewThreads(first: 100, after: $cursor) { pageInfo { hasNextPage endCursor } nodes { line diffSide startLine startDiffSide comments(first: 100) { nodes { id } } } } } } } mutation SaveDraftComment($input: AddPullRequestReviewThreadInput!) { addPullRequestReviewThread(input: $input) { thread { comments(first: 1) { nodes { id url } } } } } mutation CreateDraftReview($input: AddPullRequestReviewInput!) { addPullRequestReview(input: $input) { pullRequestReview { id comments(first: 1) { nodes { id url } } } } } mutation DiscardDraftReview($id: ID!) { deletePullRequestReview(input: { pullRequestReviewId: $id }) { clientMutationId } } mutation DiscardDraftComment($id: ID!) { deletePullRequestReviewComment(input: { id: $id }) { clientMutationId } } mutation SubmitDraftReview($input: SubmitPullRequestReviewInput!) { submitPullRequestReview(input: $input) { pullRequestReview { id } } }