Merge pull request #10818 from cli/wm/projectsv1-deprecation-issue-create-web

Feature detect v1 projects on web mode issue create
This commit is contained in:
William Martin 2025-04-23 14:14:43 +02:00 committed by GitHub
commit 1a15212e4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 420 additions and 138 deletions

View file

@ -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"
@ -782,35 +783,54 @@ func (m *RepoMetadataResult) projectV2TitleToID(projectTitle string) (string, bo
return "", false
}
func ProjectsToPaths(projects []RepoProject, projectsV2 []ProjectV2, names []string) ([]string, error) {
var paths []string
for _, projectName := range names {
found := false
for _, p := range projects {
if strings.EqualFold(projectName, p.Name) {
// format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER
// required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER
var path string
pathParts := strings.Split(p.ResourcePath, "/")
if pathParts[1] == "orgs" || pathParts[1] == "users" {
path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
} else {
path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string, projectsV1Support gh.ProjectsV1Support) ([]string, error) {
paths := make([]string, 0, len(projectNames))
matchedPaths := map[string]struct{}{}
// TODO: ProjectsV1Cleanup
// At this point, we only know the names that the user has provided, so we can't push this conditional up the stack.
// First we'll try to match against v1 projects, if supported
if projectsV1Support == gh.ProjectsV1Supported {
v1Projects, err := v1Projects(client, repo)
if err != nil {
return nil, err
}
for _, projectName := range projectNames {
for _, p := range v1Projects {
if strings.EqualFold(projectName, p.Name) {
pathParts := strings.Split(p.ResourcePath, "/")
var path string
if pathParts[1] == "orgs" || pathParts[1] == "users" {
path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
} else {
path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
}
paths = append(paths, path)
matchedPaths[projectName] = struct{}{}
break
}
paths = append(paths, path)
found = true
break
}
}
if found {
}
// Then we'll try to match against v2 projects
v2Projects, err := v2Projects(client, repo)
if err != nil {
return nil, err
}
for _, projectName := range projectNames {
// If we already found a v1 project with this name, skip it
if _, ok := matchedPaths[projectName]; ok {
continue
}
for _, p := range projectsV2 {
found := false
for _, p := range v2Projects {
if strings.EqualFold(projectName, p.Title) {
// format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER
// required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER
var path string
pathParts := strings.Split(p.ResourcePath, "/")
var path string
if pathParts[1] == "orgs" || pathParts[1] == "users" {
path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
} else {
@ -821,10 +841,12 @@ func ProjectsToPaths(projects []RepoProject, projectsV2 []ProjectV2, names []str
break
}
}
if !found {
return nil, fmt.Errorf("'%s' not found", projectName)
}
}
return paths, nil
}
@ -1253,14 +1275,6 @@ 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)
if err != nil {
return nil, err
}
return ProjectsToPaths(projects, projectsV2, projectNames)
}
// v1Projects retrieves set of RepoProjects relevant to given repository:
// - Projects for repository
// - Projects for repository organization, if it belongs to one
@ -1366,26 +1380,6 @@ func v2Projects(client *Client, repo ghrepo.Interface) ([]ProjectV2, error) {
return projectsV2, nil
}
// relevantProjects retrieves set of Project or ProjectV2 relevant to given repository:
// - Projects for repository
// - Projects for repository organization, if it belongs to one
// - 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) {
v1Projects, err := v1Projects(client, repo)
if err != nil {
return nil, nil, err
}
v2Projects, err := v2Projects(client, repo)
if err != nil {
return nil, nil, err
}
return v1Projects, v2Projects, nil
}
func CreateRepoTransformToV4(apiClient *Client, hostname string, method string, path string, body io.Reader) (*Repository, error) {
var responsev3 repositoryV3
err := apiClient.REST(hostname, method, path, body, &responsev3)

View file

@ -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"
@ -212,37 +213,16 @@ func Test_RepoMetadata(t *testing.T) {
}
}
func Test_ProjectsToPaths(t *testing.T) {
expectedProjectPaths := []string{"OWNER/REPO/PROJECT_NUMBER", "ORG/PROJECT_NUMBER", "OWNER/REPO/PROJECT_NUMBER_2"}
projects := []RepoProject{
{ID: "id1", Name: "My Project", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER"},
{ID: "id2", Name: "Org Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER"},
{ID: "id3", Name: "Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_2"},
}
projectsV2 := []ProjectV2{
{ID: "id4", Title: "My Project V2", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER_2"},
{ID: "id5", Title: "Org Project V2", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_3"},
}
projectNames := []string{"My Project", "Org Project", "My Project V2"}
projectPaths, err := ProjectsToPaths(projects, projectsV2, projectNames)
if err != nil {
t.Errorf("error resolving projects: %v", err)
}
if !sliceEqual(projectPaths, expectedProjectPaths) {
t.Errorf("expected projects %v, got %v", expectedProjectPaths, projectPaths)
}
}
func Test_ProjectNamesToPaths(t *testing.T) {
http := &httpmock.Registry{}
client := newTestClient(http)
t.Run("when projectsV1 is supported, requests them", func(t *testing.T) {
http := &httpmock.Registry{}
client := newTestClient(http)
repo, _ := ghrepo.FromFullName("OWNER/REPO")
repo, _ := ghrepo.FromFullName("OWNER/REPO")
http.Register(
httpmock.GraphQL(`query RepositoryProjectList\b`),
httpmock.StringResponse(`
http.Register(
httpmock.GraphQL(`query RepositoryProjectList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projects": {
"nodes": [
{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" },
@ -251,9 +231,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectList\b`),
httpmock.StringResponse(`
http.Register(
httpmock.GraphQL(`query OrganizationProjectList\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projects": {
"nodes": [
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
@ -261,9 +241,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
http.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [
{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
@ -272,9 +252,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
http.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [
{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" }
@ -282,9 +262,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
http.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [
{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/users/MONALISA/projects/5" }
@ -293,15 +273,110 @@ func Test_ProjectNamesToPaths(t *testing.T) {
} } } }
`))
projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Supported)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2", "ORG/2", "OWNER/REPO/4", "MONALISA/5"}
if !sliceEqual(projectPaths, expectedProjectPaths) {
t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
}
expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2", "ORG/2", "OWNER/REPO/4", "MONALISA/5"}
if !sliceEqual(projectPaths, expectedProjectPaths) {
t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
}
})
t.Run("when projectsV1 is not supported, does not request them", func(t *testing.T) {
http := &httpmock.Registry{}
client := newTestClient(http)
repo, _ := ghrepo.FromFullName("OWNER/REPO")
http.Exclude(
t,
httpmock.GraphQL(`query RepositoryProjectList\b`),
)
http.Exclude(
t,
httpmock.GraphQL(`query OrganizationProjectList\b`),
)
http.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [
{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
{ "title": "RoadmapV2", "id": "ROADMAPV2ID", "resourcePath": "/OWNER/REPO/projects/4" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [
{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [
{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/users/MONALISA/projects/5" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
projectPaths, err := ProjectNamesToPaths(client, repo, []string{"TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Unsupported)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedProjectPaths := []string{"ORG/2", "OWNER/REPO/4", "MONALISA/5"}
if !sliceEqual(projectPaths, expectedProjectPaths) {
t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
}
})
t.Run("when a project is not found, returns an error", func(t *testing.T) {
http := &httpmock.Registry{}
client := newTestClient(http)
repo, _ := ghrepo.FromFullName("OWNER/REPO")
// No projects found
http.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
http.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
_, err := ProjectNamesToPaths(client, repo, []string{"TriageV2"}, gh.ProjectsV1Unsupported)
require.Equal(t, err, fmt.Errorf("'TriageV2' not found"))
})
}
func Test_RepoResolveMetadataIDs(t *testing.T) {

View file

@ -173,13 +173,13 @@ func createRun(opts *CreateOptions) (err error) {
}
tb := prShared.IssueMetadataState{
Type: prShared.IssueMetadata,
Assignees: assignees,
Labels: opts.Labels,
Projects: opts.Projects,
Milestones: milestones,
Title: opts.Title,
Body: opts.Body,
Type: prShared.IssueMetadata,
Assignees: assignees,
Labels: opts.Labels,
ProjectTitles: opts.Projects,
Milestones: milestones,
Title: opts.Title,
Body: opts.Body,
}
if opts.RecoverFile != "" {
@ -195,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
}
@ -273,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
}
@ -367,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)
}

View file

@ -474,6 +474,7 @@ func Test_createRun(t *testing.T) {
opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
}
opts.Detector = &fd.EnabledDetectorMock{}
browser := &browser.Stub{}
opts.Browser = browser
@ -1103,4 +1104,71 @@ func TestProjectsV1Deprecation(t *testing.T) {
reg.Verify(t)
})
})
t.Run("web mode", func(t *testing.T) {
t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
reg := &httpmock.Registry{}
reg.Register(
// ( is required to avoid matching projectsV2
httpmock.GraphQL(`projects\(`),
// Simulate a GraphQL error to early exit the test.
httpmock.StatusStringResponse(500, ""),
)
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
// Ignore the error because we have no way to really stub it without
// fully stubbing a GQL error structure in the request body.
_ = createRun(&CreateOptions{
IO: ios,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Detector: &fd.EnabledDetectorMock{},
WebMode: true,
// Required to force a lookup of projects
Projects: []string{"Project"},
})
// Verify that our request contained projects
reg.Verify(t)
})
t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
reg := &httpmock.Registry{}
// ( is required to avoid matching projectsV2
reg.Exclude(t, httpmock.GraphQL(`projects\(`))
_, cmdTeardown := run.Stub()
defer cmdTeardown(t)
// Ignore the error because we're not really interested in it.
_ = createRun(&CreateOptions{
IO: ios,
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Detector: &fd.DisabledDetectorMock{},
WebMode: true,
// Required to force a lookup of projects
Projects: []string{"Project"},
})
// Verify that our request contained projectCards
reg.Verify(t)
})
})
}

View file

@ -625,13 +625,13 @@ func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadata
}
state := &shared.IssueMetadataState{
Type: shared.PRMetadata,
Reviewers: opts.Reviewers,
Assignees: assignees,
Labels: opts.Labels,
Projects: opts.Projects,
Milestones: milestoneTitles,
Draft: opts.IsDraft,
Type: shared.PRMetadata,
Reviewers: opts.Reviewers,
Assignees: assignees,
Labels: opts.Labels,
ProjectTitles: opts.Projects,
Milestones: milestoneTitles,
Draft: opts.IsDraft,
}
if opts.FillVerbose || opts.Autofill || opts.FillFirst || !opts.TitleProvided || !opts.BodyProvided {
@ -1032,8 +1032,8 @@ func renderPullRequestPlain(w io.Writer, params map[string]interface{}, state *s
if len(state.Milestones) != 0 {
fmt.Fprintf(w, "milestones:\t%v\n", strings.Join(state.Milestones, ", "))
}
if len(state.Projects) != 0 {
fmt.Fprintf(w, "projects:\t%v\n", strings.Join(state.Projects, ", "))
if len(state.ProjectTitles) != 0 {
fmt.Fprintf(w, "projects:\t%v\n", strings.Join(state.ProjectTitles, ", "))
}
fmt.Fprintf(w, "maintainerCanModify:\t%t\n", params["maintainerCanModify"])
fmt.Fprint(w, "body:\n")
@ -1064,8 +1064,8 @@ func renderPullRequestTTY(io *iostreams.IOStreams, params map[string]interface{}
if len(state.Milestones) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Milestones"), strings.Join(state.Milestones, ", "))
}
if len(state.Projects) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), strings.Join(state.Projects, ", "))
if len(state.ProjectTitles) != 0 {
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), strings.Join(state.ProjectTitles, ", "))
}
fmt.Fprintf(out, "%s: %t\n", cs.Bold("MaintainerCanModify"), params["maintainerCanModify"])
@ -1221,7 +1221,8 @@ func generateCompareURL(ctx CreateContext, state shared.IssueMetadataState) (str
ctx.PRRefs.BaseRepo(),
"compare/%s...%s?expand=1",
url.PathEscape(ctx.PRRefs.BaseRef()), url.PathEscape(ctx.PRRefs.QualifiedHeadRef()))
url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.PRRefs.BaseRepo(), u, state)
// TODO wm: revisit project support
url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.PRRefs.BaseRepo(), u, state, gh.ProjectsV1Supported)
if err != nil {
return "", err
}

View file

@ -12,7 +12,7 @@ import (
"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,8 +35,8 @@ func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, ba
if len(state.Labels) > 0 {
q.Set("labels", strings.Join(state.Labels, ","))
}
if len(state.Projects) > 0 {
projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.Projects)
if len(state.ProjectTitles) > 0 {
projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.ProjectTitles, projectsV1Support)
if err != nil {
return "", fmt.Errorf("could not add to project: %w", err)
}
@ -72,7 +72,7 @@ func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetada
resolveInput.Labels = tb.Labels
}
if len(tb.Projects) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) {
if len(tb.ProjectTitles) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) {
if projectV1Support == gh.ProjectsV1Supported {
resolveInput.ProjectsV1 = true
}
@ -119,7 +119,7 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par
}
params["labelIds"] = labelIDs
projectIDs, projectV2IDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects)
projectIDs, projectV2IDs, err := tb.MetadataResult.ProjectsToIDs(tb.ProjectTitles)
if err != nil {
return fmt.Errorf("could not add to project: %w", err)
}

View file

@ -2,13 +2,16 @@ package shared
import (
"net/http"
"net/url"
"reflect"
"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"
"github.com/stretchr/testify/require"
)
func Test_listURLWithQuery(t *testing.T) {
@ -265,7 +268,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.ProjectsV1Supported)
if (err != nil) != tt.wantErr {
t.Errorf("WithPrAndIssueQueryParams() error = %v, wantErr %v", err, tt.wantErr)
return
@ -276,3 +279,144 @@ func Test_WithPrAndIssueQueryParams(t *testing.T) {
})
}
}
// TODO projectsV1Deprecation
// Remove this test.
func TestWithPrAndIssueQueryParamsProjectsV1Deprecation(t *testing.T) {
t.Run("when projectsV1 is supported, requests them", func(t *testing.T) {
reg := &httpmock.Registry{}
client := api.NewClientFromHTTP(&http.Client{
Transport: reg,
})
repo, _ := ghrepo.FromFullName("OWNER/REPO")
reg.Register(
httpmock.GraphQL(`query RepositoryProjectList\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projects": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query OrganizationProjectList\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projects": {
"nodes": [
{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
u, err := WithPrAndIssueQueryParams(
client,
repo,
"http://example.com/hey",
IssueMetadataState{
ProjectTitles: []string{"Triage"},
},
gh.ProjectsV1Supported,
)
require.NoError(t, err)
url, err := url.Parse(u)
require.NoError(t, err)
require.Equal(
t,
url.Query().Get("projects"),
"ORG/1",
)
})
t.Run("when projectsV1 is not supported, does not request them", func(t *testing.T) {
reg := &httpmock.Registry{}
client := api.NewClientFromHTTP(&http.Client{
Transport: reg,
})
repo, _ := ghrepo.FromFullName("OWNER/REPO")
reg.Exclude(
t,
httpmock.GraphQL(`query RepositoryProjectList\b`),
)
reg.Exclude(
t,
httpmock.GraphQL(`query OrganizationProjectList\b`),
)
reg.Register(
httpmock.GraphQL(`query RepositoryProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "repository": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query OrganizationProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "organization": { "projectsV2": {
"nodes": [
{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2" }
],
"pageInfo": { "hasNextPage": false }
} } } }
`))
reg.Register(
httpmock.GraphQL(`query UserProjectV2List\b`),
httpmock.StringResponse(`
{ "data": { "viewer": { "projectsV2": {
"nodes": [],
"pageInfo": { "hasNextPage": false }
} } } }
`))
u, err := WithPrAndIssueQueryParams(
client,
repo,
"http://example.com/hey",
IssueMetadataState{
ProjectTitles: []string{"TriageV2"},
},
gh.ProjectsV1Unsupported,
)
require.NoError(t, err)
url, err := url.Parse(u)
require.NoError(t, err)
require.Equal(
t,
url.Query().Get("projects"),
"ORG/2",
)
})
}

View file

@ -25,12 +25,12 @@ type IssueMetadataState struct {
Template string
Metadata []string
Reviewers []string
Assignees []string
Labels []string
Projects []string
Milestones []string
Metadata []string
Reviewers []string
Assignees []string
Labels []string
ProjectTitles []string
Milestones []string
MetadataResult *api.RepoMetadataResult
@ -49,7 +49,7 @@ func (tb *IssueMetadataState) HasMetadata() bool {
return len(tb.Reviewers) > 0 ||
len(tb.Assignees) > 0 ||
len(tb.Labels) > 0 ||
len(tb.Projects) > 0 ||
len(tb.ProjectTitles) > 0 ||
len(tb.Milestones) > 0
}

View file

@ -268,7 +268,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
}
if isChosen("Projects") {
if len(projects) > 0 {
selected, err := p.MultiSelect("Projects", state.Projects, projects)
selected, err := p.MultiSelect("Projects", state.ProjectTitles, projects)
if err != nil {
return err
}
@ -317,7 +317,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
state.Labels = values.Labels
}
if isChosen("Projects") {
state.Projects = values.Projects
state.ProjectTitles = values.Projects
}
if isChosen("Milestone") {
if values.Milestone != "" && values.Milestone != noMilestone {

View file

@ -80,7 +80,7 @@ func TestMetadataSurvey_selectAll(t *testing.T) {
assert.Equal(t, []string{"hubot"}, state.Assignees)
assert.Equal(t, []string{"monalisa"}, state.Reviewers)
assert.Equal(t, []string{"good first issue"}, state.Labels)
assert.Equal(t, []string{"The road to 1.0"}, state.Projects)
assert.Equal(t, []string{"The road to 1.0"}, state.ProjectTitles)
assert.Equal(t, []string{}, state.Milestones)
}
@ -125,7 +125,7 @@ func TestMetadataSurvey_keepExisting(t *testing.T) {
assert.Equal(t, []string{"hubot"}, state.Assignees)
assert.Equal(t, []string{"good first issue"}, state.Labels)
assert.Equal(t, []string{"The road to 1.0"}, state.Projects)
assert.Equal(t, []string{"The road to 1.0"}, state.ProjectTitles)
}
// TODO projectsV1Deprecation