Merge branch 'trunk' into kw/accessible-prompter-and-disable-spinners-config
This commit is contained in:
commit
5316f052d8
14 changed files with 785 additions and 193 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
|
||||
}
|
||||
|
||||
|
|
@ -863,7 +885,8 @@ type RepoMetadataInput struct {
|
|||
Assignees bool
|
||||
Reviewers bool
|
||||
Labels bool
|
||||
Projects bool
|
||||
ProjectsV1 bool
|
||||
ProjectsV2 bool
|
||||
Milestones bool
|
||||
}
|
||||
|
||||
|
|
@ -882,6 +905,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if input.Reviewers {
|
||||
g.Go(func() error {
|
||||
teams, err := OrganizationTeams(client, repo)
|
||||
|
|
@ -894,6 +918,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if input.Reviewers {
|
||||
g.Go(func() error {
|
||||
login, err := CurrentLoginName(client, repo.RepoHost())
|
||||
|
|
@ -904,6 +929,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if input.Labels {
|
||||
g.Go(func() error {
|
||||
labels, err := RepoLabels(client, repo)
|
||||
|
|
@ -914,13 +940,23 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
|
|||
return err
|
||||
})
|
||||
}
|
||||
if input.Projects {
|
||||
|
||||
if input.ProjectsV1 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
result.Projects, result.ProjectsV2, err = relevantProjects(client, repo)
|
||||
result.Projects, err = v1Projects(client, repo)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if input.ProjectsV2 {
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
result.ProjectsV2, err = v2Projects(client, repo)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
if input.Milestones {
|
||||
g.Go(func() error {
|
||||
milestones, err := RepoMilestones(client, repo, "open")
|
||||
|
|
@ -943,7 +979,8 @@ type RepoResolveInput struct {
|
|||
Assignees []string
|
||||
Reviewers []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
ProjectsV1 bool
|
||||
ProjectsV2 bool
|
||||
Milestones []string
|
||||
}
|
||||
|
||||
|
|
@ -970,7 +1007,8 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes
|
|||
|
||||
// there is no way to look up projects nor milestones by name, so preload them all
|
||||
mi := RepoMetadataInput{
|
||||
Projects: len(input.Projects) > 0,
|
||||
ProjectsV1: input.ProjectsV1,
|
||||
ProjectsV2: input.ProjectsV2,
|
||||
Milestones: len(input.Milestones) > 0,
|
||||
}
|
||||
result, err := RepoMetadata(client, repo, mi)
|
||||
|
|
@ -1237,26 +1275,12 @@ 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)
|
||||
}
|
||||
|
||||
// RelevantProjects retrieves set of Projects and ProjectsV2 relevant to given repository:
|
||||
// v1Projects retrieves set of RepoProjects 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) {
|
||||
func v1Projects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) {
|
||||
var repoProjects []RepoProject
|
||||
var orgProjects []RepoProject
|
||||
var userProjectsV2 []ProjectV2
|
||||
var repoProjectsV2 []ProjectV2
|
||||
var orgProjectsV2 []ProjectV2
|
||||
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
|
||||
|
|
@ -1268,6 +1292,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return err
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
orgProjects, err = OrganizationProjects(client, repo)
|
||||
|
|
@ -1277,6 +1302,29 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects := make([]RepoProject, 0, len(repoProjects)+len(orgProjects))
|
||||
projects = append(projects, repoProjects...)
|
||||
projects = append(projects, orgProjects...)
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
// v2Projects retrieves set of ProjectV2 relevant to given repository:
|
||||
// - ProjectsV2 owned by current user
|
||||
// - ProjectsV2 linked to repository
|
||||
// - ProjectsV2 owned by repository organization, if it belongs to one
|
||||
func v2Projects(client *Client, repo ghrepo.Interface) ([]ProjectV2, error) {
|
||||
var userProjectsV2 []ProjectV2
|
||||
var repoProjectsV2 []ProjectV2
|
||||
var orgProjectsV2 []ProjectV2
|
||||
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
userProjectsV2, err = CurrentUserProjectsV2(client, repo.RepoHost())
|
||||
|
|
@ -1286,6 +1334,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
repoProjectsV2, err = RepoProjectsV2(client, repo)
|
||||
|
|
@ -1295,6 +1344,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
var err error
|
||||
orgProjectsV2, err = OrganizationProjectsV2(client, repo)
|
||||
|
|
@ -1308,13 +1358,9 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects := make([]RepoProject, 0, len(repoProjects)+len(orgProjects))
|
||||
projects = append(projects, repoProjects...)
|
||||
projects = append(projects, orgProjects...)
|
||||
|
||||
// ProjectV2 might appear across multiple queries so use a map to keep them deduplicated.
|
||||
m := make(map[string]ProjectV2, len(userProjectsV2)+len(repoProjectsV2)+len(orgProjectsV2))
|
||||
for _, p := range userProjectsV2 {
|
||||
|
|
@ -1331,7 +1377,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
|
|||
projectsV2 = append(projectsV2, p)
|
||||
}
|
||||
|
||||
return projects, projectsV2, nil
|
||||
return projectsV2, nil
|
||||
}
|
||||
|
||||
func CreateRepoTransformToV4(apiClient *Client, hostname string, method string, path string, body io.Reader) (*Repository, error) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -44,7 +45,8 @@ func Test_RepoMetadata(t *testing.T) {
|
|||
Assignees: true,
|
||||
Reviewers: true,
|
||||
Labels: true,
|
||||
Projects: true,
|
||||
ProjectsV1: true,
|
||||
ProjectsV2: true,
|
||||
Milestones: true,
|
||||
}
|
||||
|
||||
|
|
@ -211,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" },
|
||||
|
|
@ -250,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" }
|
||||
|
|
@ -260,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" },
|
||||
|
|
@ -271,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" }
|
||||
|
|
@ -281,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" }
|
||||
|
|
@ -292,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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -24,6 +26,7 @@ type CreateOptions struct {
|
|||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser.Browser
|
||||
Prompter prShared.Prompt
|
||||
Detector fd.Detector
|
||||
TitledEditSurvey func(string, string) (string, string, error)
|
||||
|
||||
RootDirOverride string
|
||||
|
|
@ -46,11 +49,12 @@ type CreateOptions struct {
|
|||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
|
||||
opts := &CreateOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Browser: f.Browser,
|
||||
Prompter: f.Prompter,
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Browser: f.Browser,
|
||||
Prompter: f.Prompter,
|
||||
|
||||
TitledEditSurvey: prShared.TitledEditSurvey(&prShared.UserEditor{Config: f.Config, IO: f.IOStreams}),
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +150,15 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this section as we should no longer need to detect
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
||||
}
|
||||
|
||||
projectsV1Support := opts.Detector.ProjectsV1()
|
||||
|
||||
isTerminal := opts.IO.IsStdoutTTY()
|
||||
|
||||
var milestones []string
|
||||
|
|
@ -160,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 != "" {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
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/prompter"
|
||||
|
|
@ -473,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
|
||||
|
||||
|
|
@ -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 = &fd.EnabledDetectorMock{}
|
||||
return createRun(opts)
|
||||
})
|
||||
|
||||
|
|
@ -1026,3 +1029,146 @@ func TestIssueCreate_projectsV2(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
|
||||
}
|
||||
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this test.
|
||||
func TestProjectsV1Deprecation(t *testing.T) {
|
||||
|
||||
t.Run("non-interactive submission", 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.StubRepoInfoResponse("OWNER", "REPO", "main")
|
||||
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{},
|
||||
Title: "Test Title",
|
||||
Body: "Test Body",
|
||||
// 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{}
|
||||
reg.StubRepoInfoResponse("OWNER", "REPO", "main")
|
||||
// ( 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{},
|
||||
Title: "Test Title",
|
||||
Body: "Test Body",
|
||||
// Required to force a lookup of projects
|
||||
Projects: []string{"Project"},
|
||||
})
|
||||
|
||||
// Verify that our request contained projectCards
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +25,7 @@ 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
|
||||
|
|
@ -201,7 +205,18 @@ func editRun(opts *EditOptions) error {
|
|||
lookupFields = append(lookupFields, "labels")
|
||||
}
|
||||
if editable.Projects.Edited {
|
||||
lookupFields = append(lookupFields, "projectCards")
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this section as we should no longer add projectCards
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
||||
}
|
||||
|
||||
projectsV1Support := opts.Detector.ProjectsV1()
|
||||
if projectsV1Support == gh.ProjectsV1Supported {
|
||||
lookupFields = append(lookupFields, "projectCards")
|
||||
}
|
||||
|
||||
lookupFields = append(lookupFields, "projectItems")
|
||||
}
|
||||
if editable.Milestone.Edited {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ 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/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
|
|
@ -788,3 +790,88 @@ func mockProjectV2ItemUpdate(t *testing.T, reg *httpmock.Registry) {
|
|||
func(inputs map[string]interface{}) {}),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO projectsV1Deprecation
|
||||
// Remove this test.
|
||||
func TestProjectsV1Deprecation(t *testing.T) {
|
||||
t.Run("when projects v1 is supported, is included in query", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`projectCards`),
|
||||
// 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.
|
||||
_ = editRun(&EditOptions{
|
||||
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{},
|
||||
|
||||
IssueNumbers: []int{123},
|
||||
Editable: prShared.Editable{
|
||||
Projects: prShared.EditableProjects{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"Test Project"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Verify that our request contained projectCards
|
||||
reg.Verify(t)
|
||||
})
|
||||
|
||||
t.Run("when projects v1 is not supported, is not included in query", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Exclude(t, httpmock.GraphQL(`projectCards`))
|
||||
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`.*`),
|
||||
// Simulate a GraphQL error to early exit the test.
|
||||
httpmock.StatusStringResponse(500, ""),
|
||||
)
|
||||
|
||||
_, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
|
||||
// Ignore the error because we're not really interested in it.
|
||||
_ = editRun(&EditOptions{
|
||||
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{},
|
||||
|
||||
IssueNumbers: []int{123},
|
||||
Editable: prShared.Editable{
|
||||
Projects: prShared.EditableProjects{
|
||||
EditableSlice: prShared.EditableSlice{
|
||||
Add: []string{"Test Project"},
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Verify that our request contained projectCards
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -440,7 +440,8 @@ func createRun(opts *CreateOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return submitPR(*opts, *ctx, *state)
|
||||
// TODO wm: revisit project support
|
||||
return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
|
||||
}
|
||||
|
||||
if opts.RecoverFile != "" {
|
||||
|
|
@ -536,7 +537,8 @@ func createRun(opts *CreateOptions) error {
|
|||
Repo: ctx.PRRefs.BaseRepo(),
|
||||
State: state,
|
||||
}
|
||||
err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.PRRefs.BaseRepo(), fetcher, state)
|
||||
// TODO wm: revisit project support
|
||||
err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.PRRefs.BaseRepo(), fetcher, state, gh.ProjectsV1Supported)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -565,11 +567,13 @@ func createRun(opts *CreateOptions) error {
|
|||
|
||||
if action == shared.SubmitDraftAction {
|
||||
state.Draft = true
|
||||
return submitPR(*opts, *ctx, *state)
|
||||
// TODO wm: revisit project support
|
||||
return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
|
||||
}
|
||||
|
||||
if action == shared.SubmitAction {
|
||||
return submitPR(*opts, *ctx, *state)
|
||||
// TODO wm: revisit project support
|
||||
return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
|
||||
}
|
||||
|
||||
err = errors.New("expected to cancel, preview, or submit")
|
||||
|
|
@ -621,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 {
|
||||
|
|
@ -966,7 +970,7 @@ func getRemotes(opts *CreateOptions) (ghContext.Remotes, error) {
|
|||
return remotes, nil
|
||||
}
|
||||
|
||||
func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataState) error {
|
||||
func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataState, projectV1Support gh.ProjectsV1Support) error {
|
||||
client := ctx.Client
|
||||
|
||||
params := map[string]interface{}{
|
||||
|
|
@ -982,7 +986,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.PRRefs.BaseRepo(), params, &state)
|
||||
err := shared.AddMetadataToIssueParams(client, ctx.PRRefs.BaseRepo(), params, &state, projectV1Support)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -1028,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")
|
||||
|
|
@ -1060,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"])
|
||||
|
||||
|
|
@ -1217,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,7 +381,8 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable)
|
|||
Reviewers: editable.Reviewers.Edited,
|
||||
Assignees: editable.Assignees.Edited,
|
||||
Labels: editable.Labels.Edited,
|
||||
Projects: editable.Projects.Edited,
|
||||
ProjectsV1: editable.Projects.Edited,
|
||||
ProjectsV2: editable.Projects.Edited,
|
||||
Milestones: editable.Milestone.Edited,
|
||||
}
|
||||
metadata, err := api.RepoMetadata(client, repo, input)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -34,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)
|
||||
}
|
||||
|
|
@ -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, projectV1Support gh.ProjectsV1Support) error {
|
||||
resolveInput := api.RepoResolveInput{}
|
||||
|
||||
if len(tb.Assignees) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) {
|
||||
|
|
@ -71,8 +72,12 @@ 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) {
|
||||
resolveInput.Projects = tb.Projects
|
||||
if len(tb.ProjectTitles) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) {
|
||||
if projectV1Support == gh.ProjectsV1Supported {
|
||||
resolveInput.ProjectsV1 = true
|
||||
}
|
||||
|
||||
resolveInput.ProjectsV2 = true
|
||||
}
|
||||
|
||||
if len(tb.Milestones) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Milestones) == 0) {
|
||||
|
|
@ -93,12 +98,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, projectV1Support gh.ProjectsV1Support) error {
|
||||
if !tb.HasMetadata() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := fillMetadata(client, baseRepo, tb); err != nil {
|
||||
if err := fillMetadata(client, baseRepo, tb, projectV1Support); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -114,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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ type RepoMetadataFetcher interface {
|
|||
RepoMetadataFetch(api.RepoMetadataInput) (*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 {
|
||||
|
|
@ -181,7 +181,8 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
|
|||
Reviewers: isChosen("Reviewers"),
|
||||
Assignees: isChosen("Assignees"),
|
||||
Labels: isChosen("Labels"),
|
||||
Projects: isChosen("Projects"),
|
||||
ProjectsV1: isChosen("Projects") && projectsV1Support == gh.ProjectsV1Supported,
|
||||
ProjectsV2: isChosen("Projects"),
|
||||
Milestones: isChosen("Milestone"),
|
||||
}
|
||||
metadataResult, err := fetcher.RepoMetadataFetch(metadataInput)
|
||||
|
|
@ -267,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
|
||||
}
|
||||
|
|
@ -316,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 {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type metadataFetcher struct {
|
||||
|
|
@ -68,7 +71,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())
|
||||
|
|
@ -77,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)
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +116,8 @@ 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())
|
||||
|
|
@ -121,7 +125,64 @@ 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
|
||||
// Remove this test and projectsV1MetadataFetcherSpy
|
||||
func TestMetadataSurveyProjectV1Deprecation(t *testing.T) {
|
||||
t.Run("when projectsV1 is supported, requests projectsV1", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
repo := ghrepo.New("OWNER", "REPO")
|
||||
|
||||
fetcher := &projectsV1MetadataFetcherSpy{}
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterMultiSelect("What would you like to add?", []string{}, []string{"Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, options []string) ([]int, error) {
|
||||
i, err := prompter.IndexFor(options, "Projects")
|
||||
require.NoError(t, err)
|
||||
return []int{i}, nil
|
||||
})
|
||||
pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{0}, nil
|
||||
})
|
||||
|
||||
err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Supported)
|
||||
require.ErrorContains(t, err, "expected test error")
|
||||
|
||||
require.True(t, fetcher.projectsV1Requested, "expected projectsV1 to be requested")
|
||||
})
|
||||
|
||||
t.Run("when projectsV1 is supported, does not request projectsV1", func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
repo := ghrepo.New("OWNER", "REPO")
|
||||
|
||||
fetcher := &projectsV1MetadataFetcherSpy{}
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterMultiSelect("What would you like to add?", []string{}, []string{"Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, options []string) ([]int, error) {
|
||||
i, err := prompter.IndexFor(options, "Projects")
|
||||
require.NoError(t, err)
|
||||
return []int{i}, nil
|
||||
})
|
||||
pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring"}, func(_ string, _, _ []string) ([]int, error) {
|
||||
return []int{0}, nil
|
||||
})
|
||||
|
||||
err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Unsupported)
|
||||
require.ErrorContains(t, err, "expected test error")
|
||||
|
||||
require.False(t, fetcher.projectsV1Requested, "expected projectsV1 not to be requested")
|
||||
})
|
||||
}
|
||||
|
||||
type projectsV1MetadataFetcherSpy struct {
|
||||
projectsV1Requested bool
|
||||
}
|
||||
|
||||
func (mf *projectsV1MetadataFetcherSpy) RepoMetadataFetch(input api.RepoMetadataInput) (*api.RepoMetadataResult, error) {
|
||||
if input.ProjectsV1 {
|
||||
mf.projectsV1Requested = true
|
||||
}
|
||||
return nil, errors.New("expected test error")
|
||||
}
|
||||
|
||||
func TestTitledEditSurvey_cleanupHint(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import (
|
|||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -24,7 +22,7 @@ func NewClient(httpClient *http.Client, hostname string, ios *iostreams.IOStream
|
|||
}
|
||||
return &Client{
|
||||
apiClient: apiClient,
|
||||
spinner: ios.IsStdoutTTY() && ios.IsStderrTTY(),
|
||||
io: ios,
|
||||
prompter: prompter.New("", ios),
|
||||
}
|
||||
}
|
||||
|
|
@ -44,9 +42,10 @@ func NewTestClient(opts ...TestClientOpt) *Client {
|
|||
hostname: "github.com",
|
||||
Client: api.NewClientFromHTTP(http.DefaultClient),
|
||||
}
|
||||
io, _, _, _ := iostreams.Test()
|
||||
c := &Client{
|
||||
apiClient: apiClient,
|
||||
spinner: false,
|
||||
io: io,
|
||||
prompter: nil,
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +79,7 @@ type graphqlClient interface {
|
|||
|
||||
type Client struct {
|
||||
apiClient graphqlClient
|
||||
spinner bool
|
||||
io *iostreams.IOStreams
|
||||
prompter iprompter
|
||||
}
|
||||
|
||||
|
|
@ -89,19 +88,12 @@ const (
|
|||
LimitMax = 100 // https://docs.github.com/en/graphql/overview/resource-limitations#node-limit
|
||||
)
|
||||
|
||||
// doQuery wraps API calls with a visual spinner
|
||||
func (c *Client) doQuery(name string, query interface{}, variables map[string]interface{}) error {
|
||||
var sp *spinner.Spinner
|
||||
if c.spinner {
|
||||
// https://github.com/briandowns/spinner#available-character-sets
|
||||
dotStyle := spinner.CharSets[11]
|
||||
sp = spinner.New(dotStyle, 120*time.Millisecond, spinner.WithColor("fgCyan"))
|
||||
sp.Start()
|
||||
}
|
||||
// doQueryWithProgressIndicator wraps API calls with a progress indicator.
|
||||
// The query name is used in the progress indicator label.
|
||||
func (c *Client) doQueryWithProgressIndicator(name string, query interface{}, variables map[string]interface{}) error {
|
||||
c.io.StartProgressIndicatorWithLabel(fmt.Sprintf("Fetching %s", name))
|
||||
defer c.io.StopProgressIndicator()
|
||||
err := c.apiClient.Query(name, query, variables)
|
||||
if sp != nil {
|
||||
sp.Stop()
|
||||
}
|
||||
return handleError(err)
|
||||
}
|
||||
|
||||
|
|
@ -552,7 +544,7 @@ func (c *Client) ProjectItems(o *Owner, number int32, limit int) (*Project, erro
|
|||
query = &viewerOwnerWithItems{} // must be a pointer to work with graphql queries
|
||||
queryName = "ViewerProjectWithItems"
|
||||
}
|
||||
err := c.doQuery(queryName, query, variables)
|
||||
err := c.doQueryWithProgressIndicator(queryName, query, variables)
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
|
|
@ -706,7 +698,7 @@ func paginateAttributes[N projectAttribute](c *Client, p pager[N], variables map
|
|||
|
||||
// set the cursor to the end of the last page
|
||||
variables[afterKey] = (*githubv4.String)(&cursor)
|
||||
err := c.doQuery(queryName, p, variables)
|
||||
err := c.doQueryWithProgressIndicator(queryName, p, variables)
|
||||
if err != nil {
|
||||
return nodes, err
|
||||
}
|
||||
|
|
@ -863,7 +855,7 @@ func (c *Client) ProjectFields(o *Owner, number int32, limit int) (*Project, err
|
|||
query = &viewerOwnerWithFields{} // must be a pointer to work with graphql queries
|
||||
queryName = "ViewerProjectWithFields"
|
||||
}
|
||||
err := c.doQuery(queryName, query, variables)
|
||||
err := c.doQueryWithProgressIndicator(queryName, query, variables)
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
|
|
@ -977,7 +969,7 @@ const ViewerOwner OwnerType = "VIEWER"
|
|||
// ViewerLoginName returns the login name of the viewer.
|
||||
func (c *Client) ViewerLoginName() (string, error) {
|
||||
var query viewerLogin
|
||||
err := c.doQuery("Viewer", &query, map[string]interface{}{})
|
||||
err := c.doQueryWithProgressIndicator("Viewer", &query, map[string]interface{}{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -988,7 +980,7 @@ func (c *Client) ViewerLoginName() (string, error) {
|
|||
func (c *Client) OwnerIDAndType(login string) (string, OwnerType, error) {
|
||||
if login == "@me" || login == "" {
|
||||
var query viewerLogin
|
||||
err := c.doQuery("ViewerOwner", &query, nil)
|
||||
err := c.doQueryWithProgressIndicator("ViewerOwner", &query, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
|
@ -1009,7 +1001,7 @@ func (c *Client) OwnerIDAndType(login string) (string, OwnerType, error) {
|
|||
} `graphql:"organization(login: $login)"`
|
||||
}
|
||||
|
||||
err := c.doQuery("UserOrgOwner", &query, variables)
|
||||
err := c.doQueryWithProgressIndicator("UserOrgOwner", &query, variables)
|
||||
if err != nil {
|
||||
// Due to the way the queries are structured, we don't know if a login belongs to a user
|
||||
// or to an org, even though they are unique. To deal with this, we try both - if neither
|
||||
|
|
@ -1052,7 +1044,7 @@ func (c *Client) IssueOrPullRequestID(rawURL string) (string, error) {
|
|||
"url": githubv4.URI{URL: uri},
|
||||
}
|
||||
var query issueOrPullRequest
|
||||
err = c.doQuery("GetIssueOrPullRequest", &query, variables)
|
||||
err = c.doQueryWithProgressIndicator("GetIssueOrPullRequest", &query, variables)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1114,7 +1106,7 @@ func (c *Client) userOrgLogins() ([]loginTypes, error) {
|
|||
"after": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
err := c.doQuery("ViewerLoginAndOrgs", &v, variables)
|
||||
err := c.doQueryWithProgressIndicator("ViewerLoginAndOrgs", &v, variables)
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
|
@ -1152,7 +1144,7 @@ func (c *Client) paginateOrgLogins(l []loginTypes, cursor string) ([]loginTypes,
|
|||
"after": githubv4.String(cursor),
|
||||
}
|
||||
|
||||
err := c.doQuery("ViewerLoginAndOrgs", &v, variables)
|
||||
err := c.doQueryWithProgressIndicator("ViewerLoginAndOrgs", &v, variables)
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
|
@ -1247,16 +1239,16 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool)
|
|||
if o.Type == UserOwner {
|
||||
var query userOwner
|
||||
variables["login"] = githubv4.String(o.Login)
|
||||
err := c.doQuery("UserProject", &query, variables)
|
||||
err := c.doQueryWithProgressIndicator("UserProject", &query, variables)
|
||||
return &query.Owner.Project, err
|
||||
} else if o.Type == OrgOwner {
|
||||
variables["login"] = githubv4.String(o.Login)
|
||||
var query orgOwner
|
||||
err := c.doQuery("OrgProject", &query, variables)
|
||||
err := c.doQueryWithProgressIndicator("OrgProject", &query, variables)
|
||||
return &query.Owner.Project, err
|
||||
} else if o.Type == ViewerOwner {
|
||||
var query viewerOwner
|
||||
err := c.doQuery("ViewerProject", &query, variables)
|
||||
err := c.doQueryWithProgressIndicator("ViewerProject", &query, variables)
|
||||
return &query.Owner.Project, err
|
||||
}
|
||||
return nil, errors.New("unknown owner type")
|
||||
|
|
@ -1331,7 +1323,7 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Pr
|
|||
// the cost.
|
||||
if t == UserOwner {
|
||||
var query userProjects
|
||||
if err := c.doQuery("UserProjects", &query, variables); err != nil {
|
||||
if err := c.doQueryWithProgressIndicator("UserProjects", &query, variables); err != nil {
|
||||
return projects, err
|
||||
}
|
||||
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
|
||||
|
|
@ -1340,7 +1332,7 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Pr
|
|||
projects.TotalCount = query.Owner.Projects.TotalCount
|
||||
} else if t == OrgOwner {
|
||||
var query orgProjects
|
||||
if err := c.doQuery("OrgProjects", &query, variables); err != nil {
|
||||
if err := c.doQueryWithProgressIndicator("OrgProjects", &query, variables); err != nil {
|
||||
return projects, err
|
||||
}
|
||||
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
|
||||
|
|
@ -1349,7 +1341,7 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Pr
|
|||
projects.TotalCount = query.Owner.Projects.TotalCount
|
||||
} else if t == ViewerOwner {
|
||||
var query viewerProjects
|
||||
if err := c.doQuery("ViewerProjects", &query, variables); err != nil {
|
||||
if err := c.doQueryWithProgressIndicator("ViewerProjects", &query, variables); err != nil {
|
||||
return projects, err
|
||||
}
|
||||
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue