From bcfde2cc435eba00024a432b408f92e68d64233f Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 21 Aug 2024 08:09:57 -0400 Subject: [PATCH] Only request projects v1 objects for GHES --- api/queries_repo.go | 53 +++++++++++-------- api/queries_repo_test.go | 7 +-- internal/featuredetection/detector_mock.go | 10 ++++ .../featuredetection/feature_detection.go | 12 +++++ .../feature_detection_test.go | 20 +++++++ internal/gh/projects.go | 23 ++++++++ pkg/cmd/issue/close/close.go | 3 +- pkg/cmd/issue/comment/comment.go | 2 +- pkg/cmd/issue/create/create.go | 25 ++++++--- pkg/cmd/issue/create/create_test.go | 3 ++ pkg/cmd/issue/delete/delete.go | 4 +- pkg/cmd/issue/develop/develop.go | 4 +- pkg/cmd/issue/edit/edit.go | 25 +++++++-- pkg/cmd/issue/edit/edit_test.go | 2 + pkg/cmd/issue/lock/lock.go | 4 +- pkg/cmd/issue/pin/pin.go | 2 +- pkg/cmd/issue/reopen/reopen.go | 2 +- pkg/cmd/issue/shared/lookup.go | 27 +++++++--- pkg/cmd/issue/shared/lookup_test.go | 5 +- pkg/cmd/issue/transfer/transfer.go | 4 +- pkg/cmd/issue/unpin/unpin.go | 4 +- pkg/cmd/issue/view/view.go | 8 +-- pkg/cmd/issue/view/view_test.go | 7 ++- pkg/cmd/pr/create/create.go | 19 +++++-- pkg/cmd/pr/create/create_test.go | 2 + pkg/cmd/pr/edit/edit.go | 20 +++++-- pkg/cmd/pr/edit/edit_test.go | 7 ++- pkg/cmd/pr/shared/commentable.go | 2 + pkg/cmd/pr/shared/completion.go | 3 +- pkg/cmd/pr/shared/editable.go | 5 +- pkg/cmd/pr/shared/finder.go | 21 ++++++-- pkg/cmd/pr/shared/params.go | 13 ++--- pkg/cmd/pr/shared/params_test.go | 3 +- pkg/cmd/pr/shared/survey.go | 10 ++-- pkg/cmd/pr/shared/survey_test.go | 7 +-- 35 files changed, 281 insertions(+), 87 deletions(-) create mode 100644 internal/gh/projects.go diff --git a/api/queries_repo.go b/api/queries_repo.go index 53e6d879a47..5b3e0c7abfa 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghinstance" "golang.org/x/sync/errgroup" @@ -868,7 +869,10 @@ type RepoMetadataInput struct { } // RepoMetadata pre-fetches the metadata for attaching to issues and pull requests -func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput) (*RepoMetadataResult, error) { +// It's a bit of a smell that we have the information already as to whether projects are supported, and instead of adjusting `RepoMetadataInput` to signify this split, +// we pass **another** piece of information. That said, this code is rarely touched, and it's highly likely the next time we adjust this will be when GHES 3.16 is no +// longer supported, and we can just remove everything related to v1 in one go. +func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput, projectsV1Support gh.ProjectsV1Support) (*RepoMetadataResult, error) { var result RepoMetadataResult var g errgroup.Group @@ -917,7 +921,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput if input.Projects { g.Go(func() error { var err error - result.Projects, result.ProjectsV2, err = relevantProjects(client, repo) + result.Projects, result.ProjectsV2, err = relevantProjects(client, repo, projectsV1Support) return err }) } @@ -948,7 +952,7 @@ type RepoResolveInput struct { } // RepoResolveMetadataIDs looks up GraphQL node IDs in bulk -func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoResolveInput) (*RepoMetadataResult, error) { +func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoResolveInput, projectsV1Support gh.ProjectsV1Support) (*RepoMetadataResult, error) { users := input.Assignees hasUser := func(target string) bool { for _, u := range users { @@ -973,7 +977,7 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes Projects: len(input.Projects) > 0, Milestones: len(input.Milestones) > 0, } - result, err := RepoMetadata(client, repo, mi) + result, err := RepoMetadata(client, repo, mi, projectsV1Support) if err != nil { return result, err } @@ -1237,8 +1241,8 @@ func RepoMilestones(client *Client, repo ghrepo.Interface, state string) ([]Repo return milestones, nil } -func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) { - projects, projectsV2, err := relevantProjects(client, repo) +func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string, projectsV1Support gh.ProjectsV1Support) ([]string, error) { + projects, projectsV2, err := relevantProjects(client, repo, projectsV1Support) if err != nil { return nil, err } @@ -1251,7 +1255,7 @@ func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []s // - ProjectsV2 owned by current user // - ProjectsV2 linked to repository // - ProjectsV2 owned by repository organization, if it belongs to one -func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []ProjectV2, error) { +func relevantProjects(client *Client, repo ghrepo.Interface, projectsV1Support gh.ProjectsV1Support) ([]RepoProject, []ProjectV2, error) { var repoProjects []RepoProject var orgProjects []RepoProject var userProjectsV2 []ProjectV2 @@ -1260,23 +1264,26 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P g, _ := errgroup.WithContext(context.Background()) - g.Go(func() error { - var err error - repoProjects, err = RepoProjects(client, repo) - if err != nil { - err = fmt.Errorf("error fetching repo projects (classic): %w", err) - } - return err - }) - g.Go(func() error { - var err error - orgProjects, err = OrganizationProjects(client, repo) - if err != nil && !strings.Contains(err.Error(), errorResolvingOrganization) { - err = fmt.Errorf("error fetching organization projects (classic): %w", err) + if projectsV1Support == gh.ProjectsV1Supported { + g.Go(func() error { + var err error + repoProjects, err = RepoProjects(client, repo) + if err != nil { + err = fmt.Errorf("error fetching repo projects (classic): %w", err) + } return err - } - return nil - }) + }) + g.Go(func() error { + var err error + orgProjects, err = OrganizationProjects(client, repo) + if err != nil && !strings.Contains(err.Error(), errorResolvingOrganization) { + err = fmt.Errorf("error fetching organization projects (classic): %w", err) + return err + } + return nil + }) + } + g.Go(func() error { var err error userProjectsV2, err = CurrentUserProjectsV2(client, repo.RepoHost()) diff --git a/api/queries_repo_test.go b/api/queries_repo_test.go index 13aee459a1e..922090c6c09 100644 --- a/api/queries_repo_test.go +++ b/api/queries_repo_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" @@ -151,7 +152,7 @@ func Test_RepoMetadata(t *testing.T) { { "data": { "viewer": { "login": "monalisa" } } } `)) - result, err := RepoMetadata(client, repo, input) + result, err := RepoMetadata(client, repo, input, gh.ProjectsV1Supported) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -292,7 +293,7 @@ func Test_ProjectNamesToPaths(t *testing.T) { } } } } `)) - projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"}) + projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Supported) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -352,7 +353,7 @@ t001: team(slug:"robots"){id,slug} } })) - result, err := RepoResolveMetadataIDs(client, repo, input) + result, err := RepoResolveMetadataIDs(client, repo, input, gh.ProjectsV1Supported) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/internal/featuredetection/detector_mock.go b/internal/featuredetection/detector_mock.go index 6f36dd3fc03..4ba51a3c8d8 100644 --- a/internal/featuredetection/detector_mock.go +++ b/internal/featuredetection/detector_mock.go @@ -1,5 +1,7 @@ package featuredetection +import "github.com/cli/cli/v2/internal/gh" + type DisabledDetectorMock struct{} func (md *DisabledDetectorMock) IssueFeatures() (IssueFeatures, error) { @@ -14,6 +16,10 @@ func (md *DisabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error) return RepositoryFeatures{}, nil } +func (md *DisabledDetectorMock) ProjectsV1() (gh.ProjectsV1Support, error) { + return gh.ProjectsV1Unsupported, nil +} + type EnabledDetectorMock struct{} func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) { @@ -27,3 +33,7 @@ func (md *EnabledDetectorMock) PullRequestFeatures() (PullRequestFeatures, error func (md *EnabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error) { return allRepositoryFeatures, nil } + +func (md *EnabledDetectorMock) ProjectsV1() (gh.ProjectsV1Support, error) { + return gh.ProjectsV1Supported, nil +} diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index a9bbe25f851..c19ba771043 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" "golang.org/x/sync/errgroup" ghauth "github.com/cli/go-gh/v2/pkg/auth" @@ -13,6 +14,7 @@ type Detector interface { IssueFeatures() (IssueFeatures, error) PullRequestFeatures() (PullRequestFeatures, error) RepositoryFeatures() (RepositoryFeatures, error) + ProjectsV1() (gh.ProjectsV1Support, error) } type IssueFeatures struct { @@ -199,3 +201,13 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) { return features, nil } + +func (d *detector) ProjectsV1() (gh.ProjectsV1Support, error) { + // Currently, projects v1 support is entirely dependent on the host. As this is deprecated in GHES, + // we will do feature detection on whether the GHES version has support. + if ghauth.IsEnterprise(d.host) { + return gh.ProjectsV1Supported, nil + } + + return gh.ProjectsV1Unsupported, nil +} diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go index 8af091c3f01..005b87b8ad2 100644 --- a/internal/featuredetection/feature_detection_test.go +++ b/internal/featuredetection/feature_detection_test.go @@ -5,8 +5,10 @@ import ( "testing" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIssueFeatures(t *testing.T) { @@ -366,3 +368,21 @@ func TestRepositoryFeatures(t *testing.T) { }) } } + +func TestProjectV1Support(t *testing.T) { + t.Parallel() + + t.Run("when the host is enterprise, project v1 is supported", func(t *testing.T) { + detector := detector{host: "my.ghes.com"} + isProjectV1Supported, err := detector.ProjectsV1() + require.NoError(t, err) + require.Equal(t, gh.ProjectsV1Supported, isProjectV1Supported) + }) + + t.Run("when the host is not enterprise, project v1 is not supported", func(t *testing.T) { + detector := detector{host: "github.com"} + isProjectV1Supported, err := detector.ProjectsV1() + require.NoError(t, err) + require.Equal(t, gh.ProjectsV1Unsupported, isProjectV1Supported) + }) +} diff --git a/internal/gh/projects.go b/internal/gh/projects.go new file mode 100644 index 00000000000..34acf8d7c58 --- /dev/null +++ b/internal/gh/projects.go @@ -0,0 +1,23 @@ +package gh + +// ProjectsV1Support provides type safety and readability around whether or not Projects v1 is supported +// by the targeted host. +// +// It is a sealed type to ensure that consumers must use the exported ProjectsV1Supported and ProjectsV1Unsupported +// variables to get an instance of the type. +type ProjectsV1Support interface { + sealed() +} + +type projectsV1Supported struct{} + +func (projectsV1Supported) sealed() {} + +type projectsV1Unsupported struct{} + +func (projectsV1Unsupported) sealed() {} + +var ( + ProjectsV1Supported ProjectsV1Support = projectsV1Supported{} + ProjectsV1Unsupported ProjectsV1Support = projectsV1Unsupported{} +) diff --git a/pkg/cmd/issue/close/close.go b/pkg/cmd/issue/close/close.go index 9197abff661..5e4845d48c8 100644 --- a/pkg/cmd/issue/close/close.go +++ b/pkg/cmd/issue/close/close.go @@ -67,7 +67,7 @@ func closeRun(opts *CloseOptions) error { return err } - issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "state"}) + issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "state"}, opts.Detector) if err != nil { return err } @@ -109,6 +109,7 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue, } if reason != "" { + // Should this be moved up into closeRun()? if detector == nil { cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) detector = fd.NewDetector(cachedClient, repo.RepoHost()) diff --git a/pkg/cmd/issue/comment/comment.go b/pkg/cmd/issue/comment/comment.go index 090b0748c4b..414dfaccf9a 100644 --- a/pkg/cmd/issue/comment/comment.go +++ b/pkg/cmd/issue/comment/comment.go @@ -45,7 +45,7 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e if opts.EditLast { fields = append(fields, "comments") } - return issueShared.IssueFromArgWithFields(httpClient, f.BaseRepo, args[0], fields) + return issueShared.IssueFromArgWithFields(httpClient, f.BaseRepo, args[0], fields, opts.Detector) } return prShared.CommentablePreRun(cmd, opts) }, diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go index 2e3e0de519a..e6c28cce2d9 100644 --- a/pkg/cmd/issue/create/create.go +++ b/pkg/cmd/issue/create/create.go @@ -4,10 +4,12 @@ import ( "errors" "fmt" "net/http" + "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/browser" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/text" @@ -25,6 +27,7 @@ type CreateOptions struct { Browser browser.Browser Prompter prShared.Prompt TitledEditSurvey func(string, string) (string, string, error) + Detector fd.Detector RootDirOverride string @@ -146,6 +149,16 @@ func createRun(opts *CreateOptions) (err error) { return } + if opts.Detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + } + + projectsV1Support, err := opts.Detector.ProjectsV1() + if err != nil { + return err + } + isTerminal := opts.IO.IsStdoutTTY() var milestones []string @@ -182,7 +195,7 @@ func createRun(opts *CreateOptions) (err error) { if opts.WebMode { var openURL string if opts.Title != "" || opts.Body != "" || tb.HasMetadata() { - openURL, err = generatePreviewURL(apiClient, baseRepo, tb) + openURL, err = generatePreviewURL(apiClient, baseRepo, tb, projectsV1Support) if err != nil { return } @@ -260,7 +273,7 @@ func createRun(opts *CreateOptions) (err error) { } } - openURL, err = generatePreviewURL(apiClient, baseRepo, tb) + openURL, err = generatePreviewURL(apiClient, baseRepo, tb, projectsV1Support) if err != nil { return } @@ -279,7 +292,7 @@ func createRun(opts *CreateOptions) (err error) { Repo: baseRepo, State: &tb, } - err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb) + err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb, projectsV1Support) if err != nil { return } @@ -335,7 +348,7 @@ func createRun(opts *CreateOptions) (err error) { params["issueTemplate"] = templateNameForSubmit } - err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb) + err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb, projectsV1Support) if err != nil { return } @@ -354,7 +367,7 @@ func createRun(opts *CreateOptions) (err error) { return } -func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState) (string, error) { +func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState, projectsV1Support gh.ProjectsV1Support) (string, error) { openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new") - return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb) + return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb, projectsV1Support) } diff --git a/pkg/cmd/issue/create/create_test.go b/pkg/cmd/issue/create/create_test.go index 8e49700a012..2295c4c50f7 100644 --- a/pkg/cmd/issue/create/create_test.go +++ b/pkg/cmd/issue/create/create_test.go @@ -14,6 +14,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" @@ -475,6 +476,7 @@ func Test_createRun(t *testing.T) { } browser := &browser.Stub{} opts.Browser = browser + opts.Detector = &featuredetection.EnabledDetectorMock{} err := createRun(opts) if tt.wantsErr == "" { @@ -521,6 +523,7 @@ func runCommandWithRootDirOverridden(rt http.RoundTripper, isTTY bool, cli strin cmd := NewCmdCreate(factory, func(opts *CreateOptions) error { opts.RootDirOverride = rootDir + opts.Detector = &featuredetection.EnabledDetectorMock{} return createRun(opts) }) diff --git a/pkg/cmd/issue/delete/delete.go b/pkg/cmd/issue/delete/delete.go index fb41f288e56..6976b7e04a5 100644 --- a/pkg/cmd/issue/delete/delete.go +++ b/pkg/cmd/issue/delete/delete.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -20,6 +21,7 @@ type DeleteOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Prompter iprompter + Detector fd.Detector SelectorArg string Confirmed bool @@ -71,7 +73,7 @@ func deleteRun(opts *DeleteOptions) error { return err } - issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title"}) + issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title"}, opts.Detector) if err != nil { return err } diff --git a/pkg/cmd/issue/develop/develop.go b/pkg/cmd/issue/develop/develop.go index 1536800f072..a37d35a421a 100644 --- a/pkg/cmd/issue/develop/develop.go +++ b/pkg/cmd/issue/develop/develop.go @@ -9,6 +9,7 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -23,6 +24,7 @@ type DevelopOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Remotes func() (context.Remotes, error) + Detector fd.Detector IssueSelector string Name string @@ -132,7 +134,7 @@ func developRun(opts *DevelopOptions) error { } opts.IO.StartProgressIndicator() - issue, issueRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.IssueSelector, []string{"id", "number"}) + issue, issueRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.IssueSelector, []string{"id", "number"}, opts.Detector) opts.IO.StopProgressIndicator() if err != nil { return err diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 18067319f40..42bbbb340c2 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -5,9 +5,12 @@ import ( "net/http" "sort" "sync" + "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/text" shared "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -22,11 +25,12 @@ type EditOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Prompter prShared.EditPrompter + Detector fd.Detector DetermineEditor func() (string, error) FieldsToEditSurvey func(prShared.EditPrompter, *prShared.Editable) error EditFieldsSurvey func(prShared.EditPrompter, *prShared.Editable, string) error - FetchOptions func(*api.Client, ghrepo.Interface, *prShared.Editable) error + FetchOptions func(*api.Client, ghrepo.Interface, *prShared.Editable, gh.ProjectsV1Support) error SelectorArgs []string Interactive bool @@ -167,6 +171,21 @@ func editRun(opts *EditOptions) error { return err } + baseRepo, err := opts.BaseRepo() + if err != nil { + return err + } + + if opts.Detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + } + + projectsV1Support, err := opts.Detector.ProjectsV1() + if err != nil { + return err + } + // Prompt the user which fields they'd like to edit. editable := opts.Editable if opts.Interactive { @@ -192,7 +211,7 @@ func editRun(opts *EditOptions) error { } // Get all specified issues and make sure they are within the same repo. - issues, repo, err := shared.IssuesFromArgsWithFields(httpClient, opts.BaseRepo, opts.SelectorArgs, lookupFields) + issues, repo, err := shared.IssuesFromArgsWithFields(httpClient, opts.BaseRepo, opts.SelectorArgs, lookupFields, opts.Detector) if err != nil { return err } @@ -200,7 +219,7 @@ func editRun(opts *EditOptions) error { // Fetch editable shared fields once for all issues. apiClient := api.NewClientFromHTTP(httpClient) opts.IO.StartProgressIndicatorWithLabel("Fetching repository information") - err = opts.FetchOptions(apiClient, repo, &editable) + err = opts.FetchOptions(apiClient, repo, &editable, projectsV1Support) opts.IO.StopProgressIndicator() if err != nil { return err diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index 40fe6491cbe..2f74944dbe2 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -10,6 +10,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -577,6 +578,7 @@ func Test_editRun(t *testing.T) { tt.input.IO = ios tt.input.HttpClient = httpClient tt.input.BaseRepo = baseRepo + tt.input.Detector = &featuredetection.EnabledDetectorMock{} err := editRun(tt.input) if tt.wantErr { diff --git a/pkg/cmd/issue/lock/lock.go b/pkg/cmd/issue/lock/lock.go index 4e0dac05815..d7df78afca4 100644 --- a/pkg/cmd/issue/lock/lock.go +++ b/pkg/cmd/issue/lock/lock.go @@ -17,6 +17,7 @@ import ( "strings" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -96,6 +97,7 @@ type LockOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Prompter iprompter + Detector fd.Detector ParentCmd string Reason string @@ -214,7 +216,7 @@ func lockRun(state string, opts *LockOptions) error { return err } - issuePr, baseRepo, err := issueShared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, fields()) + issuePr, baseRepo, err := issueShared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, fields(), opts.Detector) parent := alias[opts.ParentCmd] diff --git a/pkg/cmd/issue/pin/pin.go b/pkg/cmd/issue/pin/pin.go index dfb11a8811c..6488e43028e 100644 --- a/pkg/cmd/issue/pin/pin.go +++ b/pkg/cmd/issue/pin/pin.go @@ -73,7 +73,7 @@ func pinRun(opts *PinOptions) error { return err } - issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "isPinned"}) + issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "isPinned"}, nil) if err != nil { return err } diff --git a/pkg/cmd/issue/reopen/reopen.go b/pkg/cmd/issue/reopen/reopen.go index 92f18a7d979..c39e6076455 100644 --- a/pkg/cmd/issue/reopen/reopen.go +++ b/pkg/cmd/issue/reopen/reopen.go @@ -64,7 +64,7 @@ func reopenRun(opts *ReopenOptions) error { return err } - issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "state"}) + issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "state"}, nil) if err != nil { return err } diff --git a/pkg/cmd/issue/shared/lookup.go b/pkg/cmd/issue/shared/lookup.go index be79f9a73e6..c80759aef87 100644 --- a/pkg/cmd/issue/shared/lookup.go +++ b/pkg/cmd/issue/shared/lookup.go @@ -12,6 +12,7 @@ import ( "github.com/cli/cli/v2/api" fd "github.com/cli/cli/v2/internal/featuredetection" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/set" "golang.org/x/sync/errgroup" @@ -19,7 +20,7 @@ import ( // IssueFromArgWithFields loads an issue or pull request with the specified fields. If some of the fields // could not be fetched by GraphQL, this returns a non-nil issue and a *PartialLoadError. -func IssueFromArgWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), arg string, fields []string) (*api.Issue, ghrepo.Interface, error) { +func IssueFromArgWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), arg string, fields []string, detector fd.Detector) (*api.Issue, ghrepo.Interface, error) { issueNumber, baseRepo, err := IssueNumberAndRepoFromArg(arg) if err != nil { return nil, nil, err @@ -32,13 +33,13 @@ func IssueFromArgWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.I } } - issue, err := findIssueOrPR(httpClient, baseRepo, issueNumber, fields) + issue, err := findIssueOrPR(httpClient, baseRepo, issueNumber, fields, detector) return issue, baseRepo, err } // IssuesFromArgsWithFields loads 1 or more issues or pull requests with the specified fields. If some of the fields // could not be fetched by GraphQL, this returns non-nil issues and a *PartialLoadError. -func IssuesFromArgsWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), args []string, fields []string) ([]*api.Issue, ghrepo.Interface, error) { +func IssuesFromArgsWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), args []string, fields []string, detector fd.Detector) ([]*api.Issue, ghrepo.Interface, error) { var issuesRepo ghrepo.Interface issueNumbers := make([]int, 0, len(args)) @@ -75,7 +76,7 @@ func IssuesFromArgsWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo for _, num := range issueNumbers { issueNumber := num g.Go(func() error { - issue, err := findIssueOrPR(httpClient, issuesRepo, issueNumber, fields) + issue, err := findIssueOrPR(httpClient, issuesRepo, issueNumber, fields, detector) if err != nil { return err } @@ -142,12 +143,15 @@ type PartialLoadError struct { error } -func findIssueOrPR(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.Issue, error) { +func findIssueOrPR(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string, detector fd.Detector) (*api.Issue, error) { + if detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + detector = fd.NewDetector(cachedClient, repo.RepoHost()) + } + fieldSet := set.NewStringSet() fieldSet.AddValues(fields) if fieldSet.Contains("stateReason") { - cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) - detector := fd.NewDetector(cachedClient, repo.RepoHost()) features, err := detector.IssueFeatures() if err != nil { return nil, err @@ -163,6 +167,15 @@ func findIssueOrPR(httpClient *http.Client, repo ghrepo.Interface, number int, f fieldSet.Remove("projectItems") fieldSet.Add("number") } + if fieldSet.Contains("projectCards") { + projectsV1Support, err := detector.ProjectsV1() + if err != nil { + return nil, err + } + if projectsV1Support == gh.ProjectsV1Unsupported { + fieldSet.Remove("projectCards") + } + } fields = fieldSet.ToSlice() diff --git a/pkg/cmd/issue/shared/lookup_test.go b/pkg/cmd/issue/shared/lookup_test.go index 44f496de44a..1261d3bf44b 100644 --- a/pkg/cmd/issue/shared/lookup_test.go +++ b/pkg/cmd/issue/shared/lookup_test.go @@ -5,6 +5,7 @@ import ( "strings" "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/stretchr/testify/assert" @@ -193,7 +194,7 @@ func TestIssueFromArgWithFields(t *testing.T) { tt.httpStub(reg) } httpClient := &http.Client{Transport: reg} - issue, repo, err := IssueFromArgWithFields(httpClient, tt.args.baseRepoFn, tt.args.selector, []string{"number"}) + issue, repo, err := IssueFromArgWithFields(httpClient, tt.args.baseRepoFn, tt.args.selector, []string{"number"}, &fd.EnabledDetectorMock{}) if (err != nil) != tt.wantErr { t.Errorf("IssueFromArgWithFields() error = %v, wantErr %v", err, tt.wantErr) if issue == nil { @@ -289,7 +290,7 @@ func TestIssuesFromArgsWithFields(t *testing.T) { tt.httpStub(reg) } httpClient := &http.Client{Transport: reg} - issues, repo, err := IssuesFromArgsWithFields(httpClient, tt.args.baseRepoFn, tt.args.selectors, []string{"number"}) + issues, repo, err := IssuesFromArgsWithFields(httpClient, tt.args.baseRepoFn, tt.args.selectors, []string{"number"}, &fd.EnabledDetectorMock{}) if (err != nil) != tt.wantErr { t.Errorf("IssuesFromArgsWithFields() error = %v, wantErr %v", err, tt.wantErr) if issues == nil { diff --git a/pkg/cmd/issue/transfer/transfer.go b/pkg/cmd/issue/transfer/transfer.go index 140d02b9194..f2c70181445 100644 --- a/pkg/cmd/issue/transfer/transfer.go +++ b/pkg/cmd/issue/transfer/transfer.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -19,6 +20,7 @@ type TransferOptions struct { Config func() (gh.Config, error) IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) + Detector fd.Detector IssueSelector string DestRepoSelector string @@ -57,7 +59,7 @@ func transferRun(opts *TransferOptions) error { return err } - issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.IssueSelector, []string{"id", "number"}) + issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.IssueSelector, []string{"id", "number"}, opts.Detector) if err != nil { return err } diff --git a/pkg/cmd/issue/unpin/unpin.go b/pkg/cmd/issue/unpin/unpin.go index 3ac28d47cf5..8e820ed54bb 100644 --- a/pkg/cmd/issue/unpin/unpin.go +++ b/pkg/cmd/issue/unpin/unpin.go @@ -6,6 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -21,6 +22,7 @@ type UnpinOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) SelectorArg string + Detector fd.Detector } func NewCmdUnpin(f *cmdutil.Factory, runF func(*UnpinOptions) error) *cobra.Command { @@ -73,7 +75,7 @@ func unpinRun(opts *UnpinOptions) error { return err } - issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "isPinned"}) + issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.SelectorArg, []string{"id", "number", "title", "isPinned"}, opts.Detector) if err != nil { return err } diff --git a/pkg/cmd/issue/view/view.go b/pkg/cmd/issue/view/view.go index 8e3aa604038..b89b137f675 100644 --- a/pkg/cmd/issue/view/view.go +++ b/pkg/cmd/issue/view/view.go @@ -12,6 +12,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/browser" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/text" issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -28,6 +29,7 @@ type ViewOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser + Detector fd.Detector SelectorArg string WebMode bool @@ -103,7 +105,7 @@ func viewRun(opts *ViewOptions) error { opts.IO.DetectTerminalTheme() opts.IO.StartProgressIndicator() - issue, baseRepo, err := findIssue(httpClient, opts.BaseRepo, opts.SelectorArg, lookupFields.ToSlice()) + issue, baseRepo, err := findIssue(httpClient, opts.BaseRepo, opts.SelectorArg, lookupFields.ToSlice(), opts.Detector) opts.IO.StopProgressIndicator() if err != nil { var loadErr *issueShared.PartialLoadError @@ -143,12 +145,12 @@ func viewRun(opts *ViewOptions) error { return printRawIssuePreview(opts.IO.Out, issue) } -func findIssue(client *http.Client, baseRepoFn func() (ghrepo.Interface, error), selector string, fields []string) (*api.Issue, ghrepo.Interface, error) { +func findIssue(client *http.Client, baseRepoFn func() (ghrepo.Interface, error), selector string, fields []string, detector fd.Detector) (*api.Issue, ghrepo.Interface, error) { fieldSet := set.NewStringSet() fieldSet.AddValues(fields) fieldSet.Add("id") - issue, repo, err := issueShared.IssueFromArgWithFields(client, baseRepoFn, selector, fieldSet.ToSlice()) + issue, repo, err := issueShared.IssueFromArgWithFields(client, baseRepoFn, selector, fieldSet.ToSlice(), detector) if err != nil { return issue, repo, err } diff --git a/pkg/cmd/issue/view/view_test.go b/pkg/cmd/issue/view/view_test.go index e1798af9f83..6847d9178c6 100644 --- a/pkg/cmd/issue/view/view_test.go +++ b/pkg/cmd/issue/view/view_test.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/run" @@ -66,7 +67,10 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err }, } - cmd := NewCmdView(factory, nil) + cmd := NewCmdView(factory, func(opts *ViewOptions) error { + opts.Detector = &featuredetection.EnabledDetectorMock{} + return viewRun(opts) + }) argv, err := shlex.Split(cli) if err != nil { @@ -274,6 +278,7 @@ func TestIssueView_tty_Preview(t *testing.T) { return ghrepo.New("OWNER", "REPO"), nil }, SelectorArg: "123", + Detector: &featuredetection.EnabledDetectorMock{}, } err := viewRun(&opts) diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index 5f8979c11c1..9d0d890cf17 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -18,6 +18,7 @@ import ( ghContext "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/browser" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/text" @@ -40,6 +41,7 @@ type CreateOptions struct { Prompter shared.Prompt Finder shared.PRFinder TitledEditSurvey func(string, string) (string, string, error) + Detector fd.Detector TitleProvided bool BodyProvided bool @@ -86,6 +88,7 @@ type CreateContext struct { IsPushEnabled bool Client *api.Client GitClient *git.Client + ProjectsV1Support gh.ProjectsV1Support } func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { @@ -442,7 +445,7 @@ func createRun(opts *CreateOptions) error { Repo: ctx.BaseRepo, State: state, } - err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.BaseRepo, fetcher, state) + err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.BaseRepo, fetcher, state, ctx.ProjectsV1Support) if err != nil { return err } @@ -777,6 +780,15 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { baseTrackingBranch = fmt.Sprintf("%s/%s", baseRemote.Name, baseBranch) } + if opts.Detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + } + projectsV1Support, err := opts.Detector.ProjectsV1() + if err != nil { + return nil, err + } + return &CreateContext{ BaseRepo: baseRepo, HeadRepo: headRepo, @@ -789,6 +801,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) { RepoContext: repoContext, Client: client, GitClient: gitClient, + ProjectsV1Support: projectsV1Support, }, nil } @@ -821,7 +834,7 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS return errors.New("pull request title must not be blank") } - err := shared.AddMetadataToIssueParams(client, ctx.BaseRepo, params, &state) + err := shared.AddMetadataToIssueParams(client, ctx.BaseRepo, params, &state, ctx.ProjectsV1Support) if err != nil { return err } @@ -1058,7 +1071,7 @@ func generateCompareURL(ctx CreateContext, state shared.IssueMetadataState) (str ctx.BaseRepo, "compare/%s...%s?expand=1", url.PathEscape(ctx.BaseBranch), url.PathEscape(ctx.HeadBranchLabel)) - url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.BaseRepo, u, state) + url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.BaseRepo, u, state, ctx.ProjectsV1Support) if err != nil { return "", err } diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 55012d7ddd9..4404298ae20 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -16,6 +16,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" @@ -1593,6 +1594,7 @@ func Test_createRun(t *testing.T) { GhPath: "some/path/gh", GitPath: "some/path/git", } + opts.Detector = &featuredetection.EnabledDetectorMock{} cleanSetup := func() {} if tt.setup != nil { cleanSetup = tt.setup(&opts, t) diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 3c8d73ad393..49bb26ee2ee 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -3,9 +3,11 @@ package edit import ( "fmt" "net/http" + "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" @@ -25,6 +27,7 @@ type EditOptions struct { Fetcher EditableOptionsFetcher EditorRetriever EditorRetriever Prompter shared.EditPrompter + Detector fd.Detector SelectorArg string Interactive bool @@ -230,8 +233,17 @@ func editRun(opts *EditOptions) error { } apiClient := api.NewClientFromHTTP(httpClient) + if opts.Detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, repo.RepoHost()) + } + projectsV1Support, err := opts.Detector.ProjectsV1() + if err != nil { + return err + } + opts.IO.StartProgressIndicator() - err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable) + err = opts.Fetcher.EditableOptionsFetch(apiClient, repo, &editable, projectsV1Support) opts.IO.StopProgressIndicator() if err != nil { return err @@ -311,13 +323,13 @@ func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error } type EditableOptionsFetcher interface { - EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable) error + EditableOptionsFetch(*api.Client, ghrepo.Interface, *shared.Editable, gh.ProjectsV1Support) error } type fetcher struct{} -func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error { - return shared.FetchOptions(client, repo, opts) +func (f fetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable, projectsV1Support gh.ProjectsV1Support) error { + return shared.FetchOptions(client, repo, opts, projectsV1Support) } type EditorRetriever interface { diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 3c4882961ad..e64e039671e 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/featuredetection" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" shared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" @@ -502,6 +504,7 @@ func Test_editRun(t *testing.T) { tt.input.IO = ios tt.input.HttpClient = httpClient + tt.input.Detector = &featuredetection.EnabledDetectorMock{} err := editRun(tt.input) assert.NoError(t, err) @@ -661,8 +664,8 @@ type testSurveyor struct { } type testEditorRetriever struct{} -func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error { - return shared.FetchOptions(client, repo, opts) +func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable, projectsV1Support gh.ProjectsV1Support) error { + return shared.FetchOptions(client, repo, opts, projectsV1Support) } func (s testSurveyor) FieldsToEdit(e *shared.Editable) error { diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index f909c755934..0bb38652065 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/cli/cli/v2/api" + fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/text" @@ -36,6 +37,7 @@ type Commentable interface { type CommentableOptions struct { IO *iostreams.IOStreams HttpClient func() (*http.Client, error) + Detector fd.Detector RetrieveCommentable func() (Commentable, ghrepo.Interface, error) EditSurvey func(string) (string, error) InteractiveEditSurvey func(string) (string, error) diff --git a/pkg/cmd/pr/shared/completion.go b/pkg/cmd/pr/shared/completion.go index e07abc5a73f..99bdae406d5 100644 --- a/pkg/cmd/pr/shared/completion.go +++ b/pkg/cmd/pr/shared/completion.go @@ -8,13 +8,14 @@ import ( "time" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" ) func RequestableReviewersForCompletion(httpClient *http.Client, repo ghrepo.Interface) ([]string, error) { client := api.NewClientFromHTTP(api.NewCachedHTTPClient(httpClient, time.Minute*2)) - metadata, err := api.RepoMetadata(client, repo, api.RepoMetadataInput{Reviewers: true}) + metadata, err := api.RepoMetadata(client, repo, api.RepoMetadataInput{Reviewers: true}, gh.ProjectsV1Unsupported) if err != nil { return nil, err } diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index cec3bfe8c9c..072cf5bd9ac 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/set" ) @@ -376,7 +377,7 @@ func FieldsToEditSurvey(p EditPrompter, editable *Editable) error { return nil } -func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error { +func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable, projectsV1Support gh.ProjectsV1Support) error { input := api.RepoMetadataInput{ Reviewers: editable.Reviewers.Edited, Assignees: editable.Assignees.Edited, @@ -384,7 +385,7 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) Projects: editable.Projects.Edited, Milestones: editable.Milestone.Edited, } - metadata, err := api.RepoMetadata(client, repo, input) + metadata, err := api.RepoMetadata(client, repo, input, projectsV1Support) if err != nil { return err } diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go index a5452852788..57888574d4f 100644 --- a/pkg/cmd/pr/shared/finder.go +++ b/pkg/cmd/pr/shared/finder.go @@ -16,6 +16,7 @@ import ( remotes "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" fd "github.com/cli/cli/v2/internal/featuredetection" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/set" @@ -95,6 +96,8 @@ type FindOptions struct { BaseBranch string // States lists the possible PR states to scope the PR-for-branch lookup to. States []string + // Detector determines which features are available. + Detector fd.Detector } // TODO: Does this also need the BaseBranchName? @@ -214,6 +217,11 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err return nil, nil, err } + if opts.Detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost()) + } + // TODO(josebalius): Should we be guarding here? if f.progress != nil { f.progress.StartProgressIndicator() @@ -226,9 +234,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err fields.AddValues([]string{"id", "number"}) // for additional preload queries below if fields.Contains("isInMergeQueue") || fields.Contains("isMergeQueueEnabled") { - cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) - detector := fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost()) - prFeatures, err := detector.PullRequestFeatures() + prFeatures, err := opts.Detector.PullRequestFeatures() if err != nil { return nil, nil, err } @@ -243,6 +249,15 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err getProjectItems = true fields.Remove("projectItems") } + if fields.Contains("projectCards") { + projectsV1Support, err := opts.Detector.ProjectsV1() + if err != nil { + return nil, nil, err + } + if projectsV1Support == gh.ProjectsV1Unsupported { + fields.Remove("projectCards") + } + } var pr *api.PullRequest if f.prNumber > 0 { diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index 128c51068a0..b96ff57f4ef 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -6,12 +6,13 @@ import ( "strings" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/search" "github.com/google/shlex" ) -func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState) (string, error) { +func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState, projectsV1Support gh.ProjectsV1Support) (string, error) { u, err := url.Parse(baseURL) if err != nil { return "", err @@ -35,7 +36,7 @@ func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, ba q.Set("labels", strings.Join(state.Labels, ",")) } if len(state.Projects) > 0 { - projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.Projects) + projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.Projects, projectsV1Support) if err != nil { return "", fmt.Errorf("could not add to project: %w", err) } @@ -56,7 +57,7 @@ func ValidURL(urlStr string) bool { // Ensure that tb.MetadataResult object exists and contains enough pre-fetched API data to be able // to resolve all object listed in tb to GraphQL IDs. -func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState) error { +func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState, projectsV1Support gh.ProjectsV1Support) error { resolveInput := api.RepoResolveInput{} if len(tb.Assignees) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) { @@ -79,7 +80,7 @@ func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetada resolveInput.Milestones = tb.Milestones } - metadataResult, err := api.RepoResolveMetadataIDs(client, baseRepo, resolveInput) + metadataResult, err := api.RepoResolveMetadataIDs(client, baseRepo, resolveInput, projectsV1Support) if err != nil { return err } @@ -93,12 +94,12 @@ func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetada return nil } -func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState) error { +func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState, projectsV1Support gh.ProjectsV1Support) error { if !tb.HasMetadata() { return nil } - if err := fillMetadata(client, baseRepo, tb); err != nil { + if err := fillMetadata(client, baseRepo, tb, projectsV1Support); err != nil { return err } diff --git a/pkg/cmd/pr/shared/params_test.go b/pkg/cmd/pr/shared/params_test.go index 5f5e674cc0f..fe02649d87e 100644 --- a/pkg/cmd/pr/shared/params_test.go +++ b/pkg/cmd/pr/shared/params_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" @@ -265,7 +266,7 @@ func Test_WithPrAndIssueQueryParams(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := WithPrAndIssueQueryParams(nil, nil, tt.args.baseURL, tt.args.state) + got, err := WithPrAndIssueQueryParams(nil, nil, tt.args.baseURL, tt.args.state, gh.ProjectsV1Unsupported) if (err != nil) != tt.wantErr { t.Errorf("WithPrAndIssueQueryParams() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go index ce38535d97b..e2e1635769b 100644 --- a/pkg/cmd/pr/shared/survey.go +++ b/pkg/cmd/pr/shared/survey.go @@ -139,19 +139,19 @@ type MetadataFetcher struct { State *IssueMetadataState } -func (mf *MetadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput) (*api.RepoMetadataResult, error) { +func (mf *MetadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput, projectsV1Support gh.ProjectsV1Support) (*api.RepoMetadataResult, error) { mf.IO.StartProgressIndicator() - metadataResult, err := api.RepoMetadata(mf.APIClient, mf.Repo, input) + metadataResult, err := api.RepoMetadata(mf.APIClient, mf.Repo, input, projectsV1Support) mf.IO.StopProgressIndicator() mf.State.MetadataResult = metadataResult return metadataResult, err } type RepoMetadataFetcher interface { - RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error) + RepoMetadataFetch(api.RepoMetadataInput, gh.ProjectsV1Support) (*api.RepoMetadataResult, error) } -func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState) error { +func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState, projectsV1Support gh.ProjectsV1Support) error { isChosen := func(m string) bool { for _, c := range state.Metadata { if m == c { @@ -184,7 +184,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface Projects: isChosen("Projects"), Milestones: isChosen("Milestone"), } - metadataResult, err := fetcher.RepoMetadataFetch(metadataInput) + metadataResult, err := fetcher.RepoMetadataFetch(metadataInput, projectsV1Support) if err != nil { return fmt.Errorf("error fetching metadata options: %w", err) } diff --git a/pkg/cmd/pr/shared/survey_test.go b/pkg/cmd/pr/shared/survey_test.go index d74696460a2..4a76b8032ac 100644 --- a/pkg/cmd/pr/shared/survey_test.go +++ b/pkg/cmd/pr/shared/survey_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/iostreams" @@ -14,7 +15,7 @@ type metadataFetcher struct { metadataResult *api.RepoMetadataResult } -func (mf *metadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput) (*api.RepoMetadataResult, error) { +func (mf *metadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput, projectsV1Support gh.ProjectsV1Support) (*api.RepoMetadataResult, error) { return mf.metadataResult, nil } @@ -68,7 +69,7 @@ func TestMetadataSurvey_selectAll(t *testing.T) { Assignees: []string{"hubot"}, Type: PRMetadata, } - err := MetadataSurvey(pm, ios, repo, fetcher, state) + err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) @@ -113,7 +114,7 @@ func TestMetadataSurvey_keepExisting(t *testing.T) { state := &IssueMetadataState{ Assignees: []string{"hubot"}, } - err := MetadataSurvey(pm, ios, repo, fetcher, state) + err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported) assert.NoError(t, err) assert.Equal(t, "", stdout.String())