Extract feature detection package (#5494)
This commit is contained in:
parent
f8b3ff999f
commit
539b150833
9 changed files with 492 additions and 488 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
201
pkg/cmd/pr/status/http.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue