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:
commit
1a15212e4a
10 changed files with 420 additions and 138 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue