Extract feature detection package (#5494)

This commit is contained in:
Sam Coe 2022-05-17 21:07:44 +02:00 committed by GitHub
parent f8b3ff999f
commit 539b150833
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 492 additions and 488 deletions

View file

@ -4,11 +4,10 @@ import (
"context"
"fmt"
"net/http"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/git"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/githubtemplate"
@ -109,55 +108,6 @@ func listPullRequestTemplates(httpClient *http.Client, repo ghrepo.Interface) ([
return templates, nil
}
func hasTemplateSupport(httpClient *http.Client, hostname string, isPR bool) (bool, error) {
if !ghinstance.IsEnterprise(hostname) {
return true, nil
}
var featureDetection struct {
Repository struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Repository: __type(name: \"Repository\")"`
CreateIssueInput struct {
InputFields []struct {
Name string
}
} `graphql:"CreateIssueInput: __type(name: \"CreateIssueInput\")"`
}
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(hostname), httpClient)
err := gql.QueryNamed(context.Background(), "IssueTemplates_fields", &featureDetection, nil)
if err != nil {
return false, err
}
var hasIssueQuerySupport bool
var hasIssueMutationSupport bool
var hasPullRequestQuerySupport bool
for _, field := range featureDetection.Repository.Fields {
if field.Name == "issueTemplates" {
hasIssueQuerySupport = true
}
if field.Name == "pullRequestTemplates" {
hasPullRequestQuerySupport = true
}
}
for _, field := range featureDetection.CreateIssueInput.InputFields {
if field.Name == "issueTemplate" {
hasIssueMutationSupport = true
}
}
if isPR {
return hasPullRequestQuerySupport, nil
} else {
return hasIssueQuerySupport && hasIssueMutationSupport, nil
}
}
type Template interface {
Name() string
NameForSubmit() string
@ -170,8 +120,8 @@ type templateManager struct {
allowFS bool
isPR bool
httpClient *http.Client
detector fd.Detector
cachedClient *http.Client
templates []Template
legacyTemplate Template
@ -186,14 +136,21 @@ func NewTemplateManager(httpClient *http.Client, repo ghrepo.Interface, dir stri
allowFS: allowFS,
isPR: isPR,
httpClient: httpClient,
detector: fd.NewDetector(httpClient, repo.RepoHost()),
}
}
func (m *templateManager) hasAPI() (bool, error) {
if m.cachedClient == nil {
m.cachedClient = api.NewCachedClient(m.httpClient, time.Hour*24)
if !m.isPR {
return true, nil
}
return hasTemplateSupport(m.cachedClient, m.repo.RepoHost(), m.isPR)
features, err := m.detector.RepositoryFeatures()
if err != nil {
return false, err
}
return features.PullRequestTemplateQuery, nil
}
func (m *templateManager) HasTemplates() (bool, error) {

View file

@ -6,6 +6,7 @@ import (
"path/filepath"
"testing"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/prompt"
@ -22,22 +23,6 @@ func TestTemplateManager_hasAPI(t *testing.T) {
httpClient := &http.Client{Transport: &tr}
defer tr.Verify(t)
tr.Register(
httpmock.GraphQL(`query IssueTemplates_fields\b`),
httpmock.StringResponse(`{"data":{
"Repository": {
"fields": [
{"name": "foo"},
{"name": "issueTemplates"}
]
},
"CreateIssueInput": {
"inputFields": [
{"name": "foo"},
{"name": "issueTemplate"}
]
}
}}`))
tr.Register(
httpmock.GraphQL(`query IssueTemplates\b`),
httpmock.StringResponse(`{"data":{"repository":{
@ -48,12 +33,12 @@ func TestTemplateManager_hasAPI(t *testing.T) {
}}}`))
m := templateManager{
repo: ghrepo.NewWithHost("OWNER", "REPO", "example.com"),
rootDir: rootDir,
allowFS: true,
isPR: false,
httpClient: httpClient,
cachedClient: httpClient,
repo: ghrepo.NewWithHost("OWNER", "REPO", "example.com"),
rootDir: rootDir,
allowFS: true,
isPR: false,
httpClient: httpClient,
detector: &fd.EnabledDetectorMock{},
}
hasTemplates, err := m.HasTemplates()
@ -84,16 +69,6 @@ func TestTemplateManager_hasAPI_PullRequest(t *testing.T) {
httpClient := &http.Client{Transport: &tr}
defer tr.Verify(t)
tr.Register(
httpmock.GraphQL(`query IssueTemplates_fields\b`),
httpmock.StringResponse(`{"data":{
"Repository": {
"fields": [
{"name": "foo"},
{"name": "pullRequestTemplates"}
]
}
}}`))
tr.Register(
httpmock.GraphQL(`query PullRequestTemplates\b`),
httpmock.StringResponse(`{"data":{"repository":{
@ -104,12 +79,12 @@ func TestTemplateManager_hasAPI_PullRequest(t *testing.T) {
}}}`))
m := templateManager{
repo: ghrepo.NewWithHost("OWNER", "REPO", "example.com"),
rootDir: rootDir,
allowFS: true,
isPR: true,
httpClient: httpClient,
cachedClient: httpClient,
repo: ghrepo.NewWithHost("OWNER", "REPO", "example.com"),
rootDir: rootDir,
allowFS: true,
isPR: true,
httpClient: httpClient,
detector: &fd.EnabledDetectorMock{},
}
hasTemplates, err := m.HasTemplates()

201
pkg/cmd/pr/status/http.go Normal file
View file

@ -0,0 +1,201 @@
package status
import (
"fmt"
"net/http"
"strings"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/set"
)
type requestOptions struct {
CurrentPR int
HeadRef string
Username string
Fields []string
}
type pullRequestsPayload struct {
ViewerCreated api.PullRequestAndTotalCount
ReviewRequested api.PullRequestAndTotalCount
CurrentPR *api.PullRequest
DefaultBranch string
}
func pullRequestStatus(httpClient *http.Client, repo ghrepo.Interface, options requestOptions) (*pullRequestsPayload, error) {
apiClient := api.NewClientFromHTTP(httpClient)
type edges struct {
TotalCount int
Edges []struct {
Node api.PullRequest
}
}
type response struct {
Repository struct {
DefaultBranchRef struct {
Name string
}
PullRequests edges
PullRequest *api.PullRequest
}
ViewerCreated edges
ReviewRequested edges
}
var fragments string
if len(options.Fields) > 0 {
fields := set.NewStringSet()
fields.AddValues(options.Fields)
// these are always necessary to find the PR for the current branch
fields.AddValues([]string{"isCrossRepository", "headRepositoryOwner", "headRefName"})
gr := api.PullRequestGraphQL(fields.ToSlice())
fragments = fmt.Sprintf("fragment pr on PullRequest{%s}fragment prWithReviews on PullRequest{...pr}", gr)
} else {
var err error
fragments, err = pullRequestFragment(httpClient, repo.RepoHost())
if err != nil {
return nil, err
}
}
queryPrefix := `
query PullRequestStatus($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
repository(owner: $owner, name: $repo) {
defaultBranchRef {
name
}
pullRequests(headRefName: $headRefName, first: $per_page, orderBy: { field: CREATED_AT, direction: DESC }) {
totalCount
edges {
node {
...prWithReviews
}
}
}
}
`
if options.CurrentPR > 0 {
queryPrefix = `
query PullRequestStatus($owner: String!, $repo: String!, $number: Int!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
repository(owner: $owner, name: $repo) {
defaultBranchRef {
name
}
pullRequest(number: $number) {
...prWithReviews
baseRef {
branchProtectionRule {
requiredApprovingReviewCount
}
}
}
}
`
}
query := fragments + queryPrefix + `
viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) {
totalCount: issueCount
edges {
node {
...prWithReviews
}
}
}
reviewRequested: search(query: $reviewerQuery, type: ISSUE, first: $per_page) {
totalCount: issueCount
edges {
node {
...pr
}
}
}
}
`
currentUsername := options.Username
if currentUsername == "@me" && ghinstance.IsEnterprise(repo.RepoHost()) {
var err error
currentUsername, err = api.CurrentLoginName(apiClient, repo.RepoHost())
if err != nil {
return nil, err
}
}
viewerQuery := fmt.Sprintf("repo:%s state:open is:pr author:%s", ghrepo.FullName(repo), currentUsername)
reviewerQuery := fmt.Sprintf("repo:%s state:open review-requested:%s", ghrepo.FullName(repo), currentUsername)
currentPRHeadRef := options.HeadRef
branchWithoutOwner := currentPRHeadRef
if idx := strings.Index(currentPRHeadRef, ":"); idx >= 0 {
branchWithoutOwner = currentPRHeadRef[idx+1:]
}
variables := map[string]interface{}{
"viewerQuery": viewerQuery,
"reviewerQuery": reviewerQuery,
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"headRefName": branchWithoutOwner,
"number": options.CurrentPR,
}
var resp response
err := apiClient.GraphQL(repo.RepoHost(), query, variables, &resp)
if err != nil {
return nil, err
}
var viewerCreated []api.PullRequest
for _, edge := range resp.ViewerCreated.Edges {
viewerCreated = append(viewerCreated, edge.Node)
}
var reviewRequested []api.PullRequest
for _, edge := range resp.ReviewRequested.Edges {
reviewRequested = append(reviewRequested, edge.Node)
}
var currentPR = resp.Repository.PullRequest
if currentPR == nil {
for _, edge := range resp.Repository.PullRequests.Edges {
if edge.Node.HeadLabel() == currentPRHeadRef {
currentPR = &edge.Node
break // Take the most recent PR for the current branch
}
}
}
payload := pullRequestsPayload{
ViewerCreated: api.PullRequestAndTotalCount{
PullRequests: viewerCreated,
TotalCount: resp.ViewerCreated.TotalCount,
},
ReviewRequested: api.PullRequestAndTotalCount{
PullRequests: reviewRequested,
TotalCount: resp.ReviewRequested.TotalCount,
},
CurrentPR: currentPR,
DefaultBranch: resp.Repository.DefaultBranchRef.Name,
}
return &payload, nil
}
func pullRequestFragment(httpClient *http.Client, hostname string) (string, error) {
fields := []string{
"number", "title", "state", "url", "isDraft", "isCrossRepository",
"headRefName", "headRepositoryOwner", "mergeStateStatus",
"statusCheckRollup", "requiresStrictStatusChecks",
}
reviewFields := []string{"reviewDecision", "latestReviews"}
fragments := fmt.Sprintf(`
fragment pr on PullRequest {%s}
fragment prWithReviews on PullRequest {...pr,%s}
`, api.PullRequestGraphQL(fields), api.PullRequestGraphQL(reviewFields))
return fragments, nil
}

View file

@ -67,7 +67,6 @@ func statusRun(opts *StatusOptions) error {
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
baseRepo, err := opts.BaseRepo()
if err != nil {
@ -91,7 +90,7 @@ func statusRun(opts *StatusOptions) error {
}
}
options := api.StatusOptions{
options := requestOptions{
Username: "@me",
CurrentPR: currentPRNumber,
HeadRef: currentPRHeadRef,
@ -99,7 +98,7 @@ func statusRun(opts *StatusOptions) error {
if opts.Exporter != nil {
options.Fields = opts.Exporter.Fields()
}
prPayload, err := api.PullRequestStatus(apiClient, baseRepo, options)
prPayload, err := pullRequestStatus(httpClient, baseRepo, options)
if err != nil {
return err
}