The assignees query fragment only requested id, login, and name but the GitHubUser struct includes a DatabaseID field. Since the field was never requested from the API, it always defaulted to Go's zero value (0) in JSON output. This adds databaseId to the fragment so the actual value is returned. Also adds ExportData test cases for assignees on both Issue and PullRequest to verify databaseId round-trips correctly through JSON serialization.
588 lines
12 KiB
Go
588 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/cli/cli/v2/pkg/set"
|
|
)
|
|
|
|
func squeeze(r rune) rune {
|
|
switch r {
|
|
case '\n', '\t':
|
|
return -1
|
|
default:
|
|
return r
|
|
}
|
|
}
|
|
|
|
func shortenQuery(q string) string {
|
|
return strings.Map(squeeze, q)
|
|
}
|
|
|
|
var assignedActors = shortenQuery(`
|
|
assignedActors(first: 10) {
|
|
nodes {
|
|
...on User {
|
|
id,
|
|
login,
|
|
name,
|
|
__typename
|
|
}
|
|
...on Bot {
|
|
id,
|
|
login,
|
|
__typename
|
|
}
|
|
},
|
|
totalCount
|
|
}
|
|
`)
|
|
|
|
var issueComments = shortenQuery(`
|
|
comments(first: 100) {
|
|
nodes {
|
|
id,
|
|
author{login,...on User{id,name}},
|
|
authorAssociation,
|
|
body,
|
|
createdAt,
|
|
includesCreatedEdit,
|
|
isMinimized,
|
|
minimizedReason,
|
|
reactionGroups{content,users{totalCount}},
|
|
url,
|
|
viewerDidAuthor
|
|
},
|
|
pageInfo{hasNextPage,endCursor},
|
|
totalCount
|
|
}
|
|
`)
|
|
|
|
var issueCommentLast = shortenQuery(`
|
|
comments(last: 1) {
|
|
nodes {
|
|
author{login,...on User{id,name}},
|
|
authorAssociation,
|
|
body,
|
|
createdAt,
|
|
includesCreatedEdit,
|
|
isMinimized,
|
|
minimizedReason,
|
|
reactionGroups{content,users{totalCount}}
|
|
},
|
|
totalCount
|
|
}
|
|
`)
|
|
|
|
var issueClosedByPullRequestsReferences = shortenQuery(`
|
|
closedByPullRequestsReferences(first: 100) {
|
|
nodes {
|
|
id,
|
|
number,
|
|
url,
|
|
repository {
|
|
id,
|
|
name,
|
|
owner {
|
|
id,
|
|
login
|
|
}
|
|
}
|
|
}
|
|
pageInfo{hasNextPage,endCursor}
|
|
}
|
|
`)
|
|
|
|
// prReviewRequests includes ...on Bot to support Copilot as a reviewer on github.com.
|
|
// On GHES, Bot is not part of the RequestedReviewer union, but the fragment is
|
|
// silently ignored (verified on GHES 3.19).
|
|
var prReviewRequests = shortenQuery(`
|
|
reviewRequests(first: 100) {
|
|
nodes {
|
|
requestedReviewer {
|
|
__typename,
|
|
...on User{login,name},
|
|
...on Bot{login},
|
|
...on Team{
|
|
organization{login}
|
|
name,
|
|
slug
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
|
|
var prReviews = shortenQuery(`
|
|
reviews(first: 100) {
|
|
nodes {
|
|
id,
|
|
author{login},
|
|
authorAssociation,
|
|
submittedAt,
|
|
body,
|
|
state,
|
|
commit{oid},
|
|
reactionGroups{content,users{totalCount}}
|
|
}
|
|
pageInfo{hasNextPage,endCursor}
|
|
totalCount
|
|
}
|
|
`)
|
|
|
|
var prLatestReviews = shortenQuery(`
|
|
latestReviews(first: 100) {
|
|
nodes {
|
|
author{login},
|
|
authorAssociation,
|
|
submittedAt,
|
|
body,
|
|
state
|
|
}
|
|
}
|
|
`)
|
|
|
|
var prFiles = shortenQuery(`
|
|
files(first: 100) {
|
|
nodes {
|
|
additions,
|
|
deletions,
|
|
path
|
|
}
|
|
}
|
|
`)
|
|
|
|
var prCommits = shortenQuery(`
|
|
commits(first: 100) {
|
|
nodes {
|
|
commit {
|
|
authors(first:100) {
|
|
nodes {
|
|
name,
|
|
email,
|
|
user{id,login}
|
|
}
|
|
},
|
|
messageHeadline,
|
|
messageBody,
|
|
oid,
|
|
committedDate,
|
|
authoredDate
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
|
|
var prClosingIssuesReferences = shortenQuery(`
|
|
closingIssuesReferences(first: 100) {
|
|
nodes {
|
|
id,
|
|
number,
|
|
url,
|
|
repository {
|
|
id,
|
|
name,
|
|
owner {
|
|
id,
|
|
login
|
|
}
|
|
}
|
|
}
|
|
pageInfo{hasNextPage,endCursor}
|
|
}
|
|
`)
|
|
|
|
var autoMergeRequest = shortenQuery(`
|
|
autoMergeRequest {
|
|
authorEmail,
|
|
commitBody,
|
|
commitHeadline,
|
|
mergeMethod,
|
|
enabledAt,
|
|
enabledBy{login,...on User{id,name}}
|
|
}
|
|
`)
|
|
|
|
func StatusCheckRollupGraphQLWithCountByState() string {
|
|
return shortenQuery(`
|
|
statusCheckRollup: commits(last: 1) {
|
|
nodes {
|
|
commit {
|
|
statusCheckRollup {
|
|
contexts {
|
|
checkRunCount,
|
|
checkRunCountsByState {
|
|
state,
|
|
count
|
|
},
|
|
statusContextCount,
|
|
statusContextCountsByState {
|
|
state,
|
|
count
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
}
|
|
|
|
func StatusCheckRollupGraphQLWithoutCountByState(after string) string {
|
|
var afterClause string
|
|
if after != "" {
|
|
afterClause = ",after:" + after
|
|
}
|
|
return fmt.Sprintf(shortenQuery(`
|
|
statusCheckRollup: commits(last: 1) {
|
|
nodes {
|
|
commit {
|
|
statusCheckRollup {
|
|
contexts(first:100%s) {
|
|
nodes {
|
|
__typename
|
|
...on StatusContext {
|
|
context,
|
|
state,
|
|
targetUrl,
|
|
createdAt,
|
|
description
|
|
},
|
|
...on CheckRun {
|
|
name,
|
|
checkSuite{workflowRun{workflow{name}}},
|
|
status,
|
|
conclusion,
|
|
startedAt,
|
|
completedAt,
|
|
detailsUrl
|
|
}
|
|
},
|
|
pageInfo{hasNextPage,endCursor}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`), afterClause)
|
|
}
|
|
|
|
func RequiredStatusCheckRollupGraphQL(prID, after string, includeEvent bool) string {
|
|
var afterClause string
|
|
if after != "" {
|
|
afterClause = ",after:" + after
|
|
}
|
|
eventField := "event,"
|
|
if !includeEvent {
|
|
eventField = ""
|
|
}
|
|
return fmt.Sprintf(shortenQuery(`
|
|
statusCheckRollup: commits(last: 1) {
|
|
nodes {
|
|
commit {
|
|
statusCheckRollup {
|
|
contexts(first:100%[1]s) {
|
|
nodes {
|
|
__typename
|
|
...on StatusContext {
|
|
context,
|
|
state,
|
|
targetUrl,
|
|
createdAt,
|
|
description,
|
|
isRequired(pullRequestId: %[2]s)
|
|
},
|
|
...on CheckRun {
|
|
name,
|
|
checkSuite{workflowRun{%[3]sworkflow{name}}},
|
|
status,
|
|
conclusion,
|
|
startedAt,
|
|
completedAt,
|
|
detailsUrl,
|
|
isRequired(pullRequestId: %[2]s)
|
|
}
|
|
},
|
|
pageInfo{hasNextPage,endCursor}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`), afterClause, prID, eventField)
|
|
}
|
|
|
|
var sharedIssuePRFields = []string{
|
|
"assignees",
|
|
"author",
|
|
"body",
|
|
"closed",
|
|
"comments",
|
|
"createdAt",
|
|
"closedAt",
|
|
"id",
|
|
"labels",
|
|
"milestone",
|
|
"number",
|
|
"projectCards",
|
|
"projectItems",
|
|
"reactionGroups",
|
|
"state",
|
|
"title",
|
|
"updatedAt",
|
|
"url",
|
|
}
|
|
|
|
// Some fields are only valid in the context of issues.
|
|
// They need to be enumerated separately in order to be filtered
|
|
// from existing code that expects to be able to pass Issue fields
|
|
// to PR queries, e.g. the PullRequestGraphql function.
|
|
var issueOnlyFields = []string{
|
|
"isPinned",
|
|
"stateReason",
|
|
"closedByPullRequestsReferences",
|
|
}
|
|
|
|
var IssueFields = append(sharedIssuePRFields, issueOnlyFields...)
|
|
|
|
var PullRequestFields = append(sharedIssuePRFields,
|
|
"additions",
|
|
"autoMergeRequest",
|
|
"baseRefName",
|
|
"baseRefOid",
|
|
"changedFiles",
|
|
"closingIssuesReferences",
|
|
"commits",
|
|
"deletions",
|
|
"files",
|
|
"fullDatabaseId",
|
|
"headRefName",
|
|
"headRefOid",
|
|
"headRepository",
|
|
"headRepositoryOwner",
|
|
"isCrossRepository",
|
|
"isDraft",
|
|
"latestReviews",
|
|
"maintainerCanModify",
|
|
"mergeable",
|
|
"mergeCommit",
|
|
"mergedAt",
|
|
"mergedBy",
|
|
"mergeStateStatus",
|
|
"potentialMergeCommit",
|
|
"reviewDecision",
|
|
"reviewRequests",
|
|
"reviews",
|
|
"statusCheckRollup",
|
|
)
|
|
|
|
// IssueGraphQL constructs a GraphQL query fragment for a set of issue fields.
|
|
func IssueGraphQL(fields []string) string {
|
|
var q []string
|
|
for _, field := range fields {
|
|
switch field {
|
|
case "author":
|
|
q = append(q, `author{login,...on User{id,name}}`)
|
|
case "mergedBy":
|
|
q = append(q, `mergedBy{login,...on User{id,name}}`)
|
|
case "headRepositoryOwner":
|
|
q = append(q, `headRepositoryOwner{id,login,...on User{name}}`)
|
|
case "headRepository":
|
|
q = append(q, `headRepository{id,name}`)
|
|
case "assignees":
|
|
q = append(q, `assignees(first:100){nodes{id,login,name,databaseId},totalCount}`)
|
|
case "assignedActors":
|
|
q = append(q, assignedActors)
|
|
case "labels":
|
|
q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`)
|
|
case "projectCards":
|
|
q = append(q, `projectCards(first:100){nodes{project{name}column{name}},totalCount}`)
|
|
case "projectItems":
|
|
q = append(q, `projectItems(first:100){nodes{id, project{id,title}, status:fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue{optionId,name}}},totalCount}`)
|
|
case "milestone":
|
|
q = append(q, `milestone{number,title,description,dueOn}`)
|
|
case "reactionGroups":
|
|
q = append(q, `reactionGroups{content,users{totalCount}}`)
|
|
case "mergeCommit":
|
|
q = append(q, `mergeCommit{oid}`)
|
|
case "potentialMergeCommit":
|
|
q = append(q, `potentialMergeCommit{oid}`)
|
|
case "autoMergeRequest":
|
|
q = append(q, autoMergeRequest)
|
|
case "comments":
|
|
q = append(q, issueComments)
|
|
case "lastComment": // pseudo-field
|
|
q = append(q, issueCommentLast)
|
|
case "reviewRequests":
|
|
q = append(q, prReviewRequests)
|
|
case "reviews":
|
|
q = append(q, prReviews)
|
|
case "latestReviews":
|
|
q = append(q, prLatestReviews)
|
|
case "files":
|
|
q = append(q, prFiles)
|
|
case "commits":
|
|
q = append(q, prCommits)
|
|
case "lastCommit": // pseudo-field
|
|
q = append(q, `commits(last:1){nodes{commit{oid}}}`)
|
|
case "commitsCount": // pseudo-field
|
|
q = append(q, `commits{totalCount}`)
|
|
case "requiresStrictStatusChecks": // pseudo-field
|
|
q = append(q, `baseRef{branchProtectionRule{requiresStrictStatusChecks}}`)
|
|
case "statusCheckRollup":
|
|
q = append(q, StatusCheckRollupGraphQLWithoutCountByState(""))
|
|
case "statusCheckRollupWithCountByState": // pseudo-field
|
|
q = append(q, StatusCheckRollupGraphQLWithCountByState())
|
|
case "closingIssuesReferences":
|
|
q = append(q, prClosingIssuesReferences)
|
|
case "closedByPullRequestsReferences":
|
|
q = append(q, issueClosedByPullRequestsReferences)
|
|
default:
|
|
q = append(q, field)
|
|
}
|
|
}
|
|
return strings.Join(q, ",")
|
|
}
|
|
|
|
// PullRequestGraphQL constructs a GraphQL query fragment for a set of pull request fields.
|
|
// It will try to sanitize the fields to just those available on pull request.
|
|
func PullRequestGraphQL(fields []string) string {
|
|
s := set.NewStringSet()
|
|
s.AddValues(fields)
|
|
s.RemoveValues(issueOnlyFields)
|
|
return IssueGraphQL(s.ToSlice())
|
|
}
|
|
|
|
var RepositoryFields = []string{
|
|
"id",
|
|
"name",
|
|
"nameWithOwner",
|
|
"owner",
|
|
"parent",
|
|
"templateRepository",
|
|
"description",
|
|
"homepageUrl",
|
|
"openGraphImageUrl",
|
|
"usesCustomOpenGraphImage",
|
|
"url",
|
|
"sshUrl",
|
|
"mirrorUrl",
|
|
"securityPolicyUrl",
|
|
|
|
"createdAt",
|
|
"pushedAt",
|
|
"updatedAt",
|
|
"archivedAt",
|
|
|
|
"isBlankIssuesEnabled",
|
|
"isSecurityPolicyEnabled",
|
|
"hasIssuesEnabled",
|
|
"hasProjectsEnabled",
|
|
"hasWikiEnabled",
|
|
"hasDiscussionsEnabled",
|
|
"mergeCommitAllowed",
|
|
"squashMergeAllowed",
|
|
"rebaseMergeAllowed",
|
|
|
|
"forkCount",
|
|
"stargazerCount",
|
|
"watchers",
|
|
"issues",
|
|
"pullRequests",
|
|
|
|
"codeOfConduct",
|
|
"contactLinks",
|
|
"defaultBranchRef",
|
|
"deleteBranchOnMerge",
|
|
"diskUsage",
|
|
"fundingLinks",
|
|
"isArchived",
|
|
"isEmpty",
|
|
"isFork",
|
|
"isInOrganization",
|
|
"isMirror",
|
|
"isPrivate",
|
|
"visibility",
|
|
"isTemplate",
|
|
"isUserConfigurationRepository",
|
|
"licenseInfo",
|
|
"viewerCanAdminister",
|
|
"viewerDefaultCommitEmail",
|
|
"viewerDefaultMergeMethod",
|
|
"viewerHasStarred",
|
|
"viewerPermission",
|
|
"viewerPossibleCommitEmails",
|
|
"viewerSubscription",
|
|
|
|
"repositoryTopics",
|
|
"primaryLanguage",
|
|
"languages",
|
|
"issueTemplates",
|
|
"pullRequestTemplates",
|
|
"labels",
|
|
"milestones",
|
|
"latestRelease",
|
|
|
|
"assignableUsers",
|
|
"mentionableUsers",
|
|
"projects",
|
|
"projectsV2",
|
|
|
|
// "branchProtectionRules", // too complex to expose
|
|
// "collaborators", // does it make sense to expose without affiliation filter?
|
|
}
|
|
|
|
func RepositoryGraphQL(fields []string) string {
|
|
var q []string
|
|
for _, field := range fields {
|
|
switch field {
|
|
case "codeOfConduct":
|
|
q = append(q, "codeOfConduct{key,name,url}")
|
|
case "contactLinks":
|
|
q = append(q, "contactLinks{about,name,url}")
|
|
case "fundingLinks":
|
|
q = append(q, "fundingLinks{platform,url}")
|
|
case "licenseInfo":
|
|
q = append(q, "licenseInfo{key,name,nickname}")
|
|
case "owner":
|
|
q = append(q, "owner{id,login}")
|
|
case "parent":
|
|
q = append(q, "parent{id,name,owner{id,login}}")
|
|
case "templateRepository":
|
|
q = append(q, "templateRepository{id,name,owner{id,login}}")
|
|
case "repositoryTopics":
|
|
q = append(q, "repositoryTopics(first:100){nodes{topic{name}}}")
|
|
case "issueTemplates":
|
|
q = append(q, "issueTemplates{name,title,body,about}")
|
|
case "pullRequestTemplates":
|
|
q = append(q, "pullRequestTemplates{body,filename}")
|
|
case "labels":
|
|
q = append(q, "labels(first:100){nodes{id,color,name,description}}")
|
|
case "languages":
|
|
q = append(q, "languages(first:100){edges{size,node{name}}}")
|
|
case "primaryLanguage":
|
|
q = append(q, "primaryLanguage{name}")
|
|
case "latestRelease":
|
|
q = append(q, "latestRelease{publishedAt,tagName,name,url}")
|
|
case "milestones":
|
|
q = append(q, "milestones(first:100,states:OPEN){nodes{number,title,description,dueOn}}")
|
|
case "assignableUsers":
|
|
q = append(q, "assignableUsers(first:100){nodes{id,login,name}}")
|
|
case "mentionableUsers":
|
|
q = append(q, "mentionableUsers(first:100){nodes{id,login,name}}")
|
|
case "projects":
|
|
q = append(q, "projects(first:100,states:OPEN){nodes{id,name,number,body,resourcePath}}")
|
|
case "projectsV2":
|
|
q = append(q, "projectsV2(first:100,query:\"is:open\"){nodes{id,number,title,resourcePath,closed,url}}")
|
|
case "watchers":
|
|
q = append(q, "watchers{totalCount}")
|
|
case "issues":
|
|
q = append(q, "issues(states:OPEN){totalCount}")
|
|
case "pullRequests":
|
|
q = append(q, "pullRequests(states:OPEN){totalCount}")
|
|
case "defaultBranchRef":
|
|
q = append(q, "defaultBranchRef{name}")
|
|
default:
|
|
q = append(q, field)
|
|
}
|
|
}
|
|
return strings.Join(q, ",")
|
|
}
|