Merge pull request #787 from cli/issue-pr-create-metadata
Add flags to add additional metadata to `issue/pr create`
This commit is contained in:
commit
c7f7bfc328
8 changed files with 1146 additions and 37 deletions
|
|
@ -1,6 +1,11 @@
|
|||
package api
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
// using API v3 here because the equivalent in GraphQL needs `read:org` scope
|
||||
func resolveOrganization(client *Client, orgName string) (string, error) {
|
||||
|
|
@ -22,3 +27,84 @@ func resolveOrganizationTeam(client *Client, orgName, teamSlug string) (string,
|
|||
err := client.REST("GET", fmt.Sprintf("orgs/%s/teams/%s", orgName, teamSlug), nil, &response)
|
||||
return response.Organization.NodeID, response.NodeID, err
|
||||
}
|
||||
|
||||
// OrganizationProjects fetches all open projects for an organization
|
||||
func OrganizationProjects(client *Client, owner string) ([]RepoProject, error) {
|
||||
var query struct {
|
||||
Organization struct {
|
||||
Projects struct {
|
||||
Nodes []RepoProject
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"projects(states: [OPEN], first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"`
|
||||
} `graphql:"organization(login: $owner)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(owner),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
|
||||
var projects []RepoProject
|
||||
for {
|
||||
err := v4.Query(context.Background(), &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects = append(projects, query.Organization.Projects.Nodes...)
|
||||
if !query.Organization.Projects.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Organization.Projects.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
type OrgTeam struct {
|
||||
ID string
|
||||
Slug string
|
||||
}
|
||||
|
||||
// OrganizationTeams fetches all the teams in an organization
|
||||
func OrganizationTeams(client *Client, owner string) ([]OrgTeam, error) {
|
||||
var query struct {
|
||||
Organization struct {
|
||||
Teams struct {
|
||||
Nodes []OrgTeam
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"teams(first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"`
|
||||
} `graphql:"organization(login: $owner)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(owner),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
|
||||
var teams []OrgTeam
|
||||
for {
|
||||
err := v4.Query(context.Background(), &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teams = append(teams, query.Organization.Teams.Nodes...)
|
||||
if !query.Organization.Teams.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Organization.Teams.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return teams, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -586,6 +586,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
|
|||
mutation CreatePullRequest($input: CreatePullRequestInput!) {
|
||||
createPullRequest(input: $input) {
|
||||
pullRequest {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
|
|
@ -595,7 +596,10 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
|
|||
"repositoryId": repo.ID,
|
||||
}
|
||||
for key, val := range params {
|
||||
inputParams[key] = val
|
||||
switch key {
|
||||
case "title", "body", "draft", "baseRefName", "headRefName":
|
||||
inputParams[key] = val
|
||||
}
|
||||
}
|
||||
variables := map[string]interface{}{
|
||||
"input": inputParams,
|
||||
|
|
@ -611,8 +615,70 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pr := &result.CreatePullRequest.PullRequest
|
||||
|
||||
return &result.CreatePullRequest.PullRequest, nil
|
||||
// metadata parameters aren't currently available in `createPullRequest`,
|
||||
// but they are in `updatePullRequest`
|
||||
updateParams := make(map[string]interface{})
|
||||
for key, val := range params {
|
||||
switch key {
|
||||
case "assigneeIds", "labelIds", "projectIds", "milestoneId":
|
||||
if !isBlank(val) {
|
||||
updateParams[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(updateParams) > 0 {
|
||||
updateQuery := `
|
||||
mutation UpdatePullRequest($input: UpdatePullRequestInput!) {
|
||||
updatePullRequest(input: $input) { clientMutationId }
|
||||
}`
|
||||
updateParams["pullRequestId"] = pr.ID
|
||||
variables := map[string]interface{}{
|
||||
"input": updateParams,
|
||||
}
|
||||
err := client.GraphQL(updateQuery, variables, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// reviewers are requested in yet another additional mutation
|
||||
reviewParams := make(map[string]interface{})
|
||||
if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) {
|
||||
reviewParams["userIds"] = ids
|
||||
}
|
||||
if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) {
|
||||
reviewParams["teamIds"] = ids
|
||||
}
|
||||
|
||||
if len(reviewParams) > 0 {
|
||||
reviewQuery := `
|
||||
mutation RequestReviews($input: RequestReviewsInput!) {
|
||||
requestReviews(input: $input) { clientMutationId }
|
||||
}`
|
||||
reviewParams["pullRequestId"] = pr.ID
|
||||
variables := map[string]interface{}{
|
||||
"input": reviewParams,
|
||||
}
|
||||
err := client.GraphQL(reviewQuery, variables, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func isBlank(v interface{}) bool {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
return vv == ""
|
||||
case []string:
|
||||
return len(vv) == 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func AddReview(client *Client, pr *PullRequest, input *PullRequestReviewInput) error {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,16 @@ func (r Repository) ViewerCanPush() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// ViewerCanTriage is true when the requesting user can triage issues and pull requests
|
||||
func (r Repository) ViewerCanTriage() bool {
|
||||
switch r.ViewerPermission {
|
||||
case "ADMIN", "MAINTAIN", "WRITE", "TRIAGE":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
||||
query := `
|
||||
query($owner: String!, $name: String!) {
|
||||
|
|
@ -73,6 +83,7 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
|
|||
id
|
||||
hasIssuesEnabled
|
||||
description
|
||||
viewerPermission
|
||||
}
|
||||
}`
|
||||
variables := map[string]interface{}{
|
||||
|
|
@ -388,6 +399,363 @@ func RepositoryReadme(client *Client, fullName string) (string, error) {
|
|||
|
||||
}
|
||||
|
||||
type RepoMetadataResult struct {
|
||||
AssignableUsers []RepoAssignee
|
||||
Labels []RepoLabel
|
||||
Projects []RepoProject
|
||||
Milestones []RepoMilestone
|
||||
Teams []OrgTeam
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) {
|
||||
var ids []string
|
||||
for _, assigneeLogin := range names {
|
||||
found := false
|
||||
for _, u := range m.AssignableUsers {
|
||||
if strings.EqualFold(assigneeLogin, u.Login) {
|
||||
ids = append(ids, u.ID)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", assigneeLogin)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) TeamsToIDs(names []string) ([]string, error) {
|
||||
var ids []string
|
||||
for _, teamSlug := range names {
|
||||
found := false
|
||||
slug := teamSlug[strings.IndexRune(teamSlug, '/')+1:]
|
||||
for _, t := range m.Teams {
|
||||
if strings.EqualFold(slug, t.Slug) {
|
||||
ids = append(ids, t.ID)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", teamSlug)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) LabelsToIDs(names []string) ([]string, error) {
|
||||
var ids []string
|
||||
for _, labelName := range names {
|
||||
found := false
|
||||
for _, l := range m.Labels {
|
||||
if strings.EqualFold(labelName, l.Name) {
|
||||
ids = append(ids, l.ID)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", labelName)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) ProjectsToIDs(names []string) ([]string, error) {
|
||||
var ids []string
|
||||
for _, projectName := range names {
|
||||
found := false
|
||||
for _, p := range m.Projects {
|
||||
if strings.EqualFold(projectName, p.Name) {
|
||||
ids = append(ids, p.ID)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("'%s' not found", projectName)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (m *RepoMetadataResult) MilestoneToID(title string) (string, error) {
|
||||
for _, m := range m.Milestones {
|
||||
if strings.EqualFold(title, m.Title) {
|
||||
return m.ID, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
type RepoMetadataInput struct {
|
||||
Assignees bool
|
||||
Reviewers bool
|
||||
Labels bool
|
||||
Projects bool
|
||||
Milestones bool
|
||||
}
|
||||
|
||||
// RepoMetadata pre-fetches the metadata for attaching to issues and pull requests
|
||||
func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput) (*RepoMetadataResult, error) {
|
||||
result := RepoMetadataResult{}
|
||||
errc := make(chan error)
|
||||
count := 0
|
||||
|
||||
if input.Assignees || input.Reviewers {
|
||||
count++
|
||||
go func() {
|
||||
users, err := RepoAssignableUsers(client, repo)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error fetching assignees: %w", err)
|
||||
}
|
||||
result.AssignableUsers = users
|
||||
errc <- err
|
||||
}()
|
||||
}
|
||||
if input.Reviewers {
|
||||
count++
|
||||
go func() {
|
||||
teams, err := OrganizationTeams(client, repo.RepoOwner())
|
||||
// TODO: better detection of non-org repos
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") {
|
||||
errc <- fmt.Errorf("error fetching organization teams: %w", err)
|
||||
return
|
||||
}
|
||||
result.Teams = teams
|
||||
errc <- nil
|
||||
}()
|
||||
}
|
||||
if input.Labels {
|
||||
count++
|
||||
go func() {
|
||||
labels, err := RepoLabels(client, repo)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error fetching labels: %w", err)
|
||||
}
|
||||
result.Labels = labels
|
||||
errc <- err
|
||||
}()
|
||||
}
|
||||
if input.Projects {
|
||||
count++
|
||||
go func() {
|
||||
projects, err := RepoProjects(client, repo)
|
||||
if err != nil {
|
||||
errc <- fmt.Errorf("error fetching projects: %w", err)
|
||||
return
|
||||
}
|
||||
result.Projects = projects
|
||||
|
||||
orgProjects, err := OrganizationProjects(client, repo.RepoOwner())
|
||||
// TODO: better detection of non-org repos
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") {
|
||||
errc <- fmt.Errorf("error fetching organization projects: %w", err)
|
||||
return
|
||||
}
|
||||
result.Projects = append(result.Projects, orgProjects...)
|
||||
errc <- nil
|
||||
}()
|
||||
}
|
||||
if input.Milestones {
|
||||
count++
|
||||
go func() {
|
||||
milestones, err := RepoMilestones(client, repo)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error fetching milestones: %w", err)
|
||||
}
|
||||
result.Milestones = milestones
|
||||
errc <- err
|
||||
}()
|
||||
}
|
||||
|
||||
var err error
|
||||
for i := 0; i < count; i++ {
|
||||
if e := <-errc; e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
|
||||
return &result, err
|
||||
}
|
||||
|
||||
type RepoProject struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// RepoProjects fetches all open projects for a repository
|
||||
func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Projects struct {
|
||||
Nodes []RepoProject
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"projects(states: [OPEN], first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
|
||||
var projects []RepoProject
|
||||
for {
|
||||
err := v4.Query(context.Background(), &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projects = append(projects, query.Repository.Projects.Nodes...)
|
||||
if !query.Repository.Projects.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.Projects.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
type RepoAssignee struct {
|
||||
ID string
|
||||
Login string
|
||||
}
|
||||
|
||||
// RepoAssignableUsers fetches all the assignable users for a repository
|
||||
func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
AssignableUsers struct {
|
||||
Nodes []RepoAssignee
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"assignableUsers(first: 100, after: $endCursor)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
|
||||
var users []RepoAssignee
|
||||
for {
|
||||
err := v4.Query(context.Background(), &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users = append(users, query.Repository.AssignableUsers.Nodes...)
|
||||
if !query.Repository.AssignableUsers.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.AssignableUsers.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
type RepoLabel struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// RepoLabels fetches all the labels in a repository
|
||||
func RepoLabels(client *Client, repo ghrepo.Interface) ([]RepoLabel, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Labels struct {
|
||||
Nodes []RepoLabel
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"labels(first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
|
||||
var labels []RepoLabel
|
||||
for {
|
||||
err := v4.Query(context.Background(), &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labels = append(labels, query.Repository.Labels.Nodes...)
|
||||
if !query.Repository.Labels.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.Labels.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
type RepoMilestone struct {
|
||||
ID string
|
||||
Title string
|
||||
}
|
||||
|
||||
// RepoMilestones fetches all open milestones in a repository
|
||||
func RepoMilestones(client *Client, repo ghrepo.Interface) ([]RepoMilestone, error) {
|
||||
var query struct {
|
||||
Repository struct {
|
||||
Milestones struct {
|
||||
Nodes []RepoMilestone
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
EndCursor string
|
||||
}
|
||||
} `graphql:"milestones(states: [OPEN], first: 100, after: $endCursor)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": githubv4.String(repo.RepoOwner()),
|
||||
"name": githubv4.String(repo.RepoName()),
|
||||
"endCursor": (*githubv4.String)(nil),
|
||||
}
|
||||
|
||||
v4 := githubv4.NewClient(client.http)
|
||||
|
||||
var milestones []RepoMilestone
|
||||
for {
|
||||
err := v4.Query(context.Background(), &query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
milestones = append(milestones, query.Repository.Milestones.Nodes...)
|
||||
if !query.Repository.Milestones.PageInfo.HasNextPage {
|
||||
break
|
||||
}
|
||||
variables["endCursor"] = githubv4.String(query.Repository.Milestones.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
return milestones, nil
|
||||
}
|
||||
|
||||
func isMarkdownFile(filename string) bool {
|
||||
// kind of gross, but i'm assuming that 90% of the time the suffix will just be .md. it didn't
|
||||
// seem worth executing a regex for this given that assumption.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ func init() {
|
|||
issueCreateCmd.Flags().StringP("body", "b", "",
|
||||
"Supply a body. Will prompt for one otherwise.")
|
||||
issueCreateCmd.Flags().BoolP("web", "w", false, "Open the browser to create an issue")
|
||||
issueCreateCmd.Flags().StringSliceP("assignee", "a", nil, "Assign a person by their `login`")
|
||||
issueCreateCmd.Flags().StringSliceP("label", "l", nil, "Add a label by `name`")
|
||||
issueCreateCmd.Flags().StringSliceP("project", "p", nil, "Add the issue to a project by `name`")
|
||||
issueCreateCmd.Flags().StringP("milestone", "m", "", "Add the issue to a milestone by `name`")
|
||||
|
||||
issueCmd.AddCommand(issueListCmd)
|
||||
issueListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
|
||||
|
|
@ -365,6 +369,23 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("could not parse body: %w", err)
|
||||
}
|
||||
|
||||
assignees, err := cmd.Flags().GetStringSlice("assignee")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse assignees: %w", err)
|
||||
}
|
||||
labelNames, err := cmd.Flags().GetStringSlice("label")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse labels: %w", err)
|
||||
}
|
||||
projectNames, err := cmd.Flags().GetStringSlice("project")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse projects: %w", err)
|
||||
}
|
||||
milestoneTitle, err := cmd.Flags().GetString("milestone")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse milestone: %w", err)
|
||||
}
|
||||
|
||||
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
|
||||
// TODO: move URL generation into GitHubRepository
|
||||
openURL := fmt.Sprintf("https://github.com/%s/issues/new", ghrepo.FullName(baseRepo))
|
||||
|
|
@ -397,11 +418,17 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
action := SubmitAction
|
||||
tb := issueMetadataState{
|
||||
Assignees: assignees,
|
||||
Labels: labelNames,
|
||||
Projects: projectNames,
|
||||
Milestone: milestoneTitle,
|
||||
}
|
||||
|
||||
interactive := !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body"))
|
||||
|
||||
if interactive {
|
||||
tb, err := titleBodySurvey(cmd, title, body, defaults{}, templateFiles)
|
||||
err := titleBodySurvey(cmd, &tb, apiClient, baseRepo, title, body, defaults{}, templateFiles, false, repo.ViewerCanTriage())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not collect title and/or body: %w", err)
|
||||
}
|
||||
|
|
@ -442,6 +469,28 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
"body": body,
|
||||
}
|
||||
|
||||
if tb.HasMetadata() {
|
||||
if tb.MetadataResult == nil {
|
||||
metadataInput := api.RepoMetadataInput{
|
||||
Assignees: len(tb.Assignees) > 0,
|
||||
Labels: len(tb.Labels) > 0,
|
||||
Projects: len(tb.Projects) > 0,
|
||||
Milestones: tb.Milestone != "",
|
||||
}
|
||||
|
||||
// TODO: for non-interactive mode, only translate given objects to GraphQL IDs
|
||||
tb.MetadataResult, err = api.RepoMetadata(apiClient, baseRepo, metadataInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = addMetadataToIssueParams(params, tb.MetadataResult, tb.Assignees, tb.Labels, tb.Projects, tb.Milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
newIssue, err := api.IssueCreate(apiClient, repo, params)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -455,6 +504,36 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func addMetadataToIssueParams(params map[string]interface{}, metadata *api.RepoMetadataResult, assignees, labelNames, projectNames []string, milestoneTitle string) error {
|
||||
assigneeIDs, err := metadata.MembersToIDs(assignees)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not assign user: %w", err)
|
||||
}
|
||||
params["assigneeIds"] = assigneeIDs
|
||||
|
||||
labelIDs, err := metadata.LabelsToIDs(labelNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add label: %w", err)
|
||||
}
|
||||
params["labelIds"] = labelIDs
|
||||
|
||||
projectIDs, err := metadata.ProjectsToIDs(projectNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add to project: %w", err)
|
||||
}
|
||||
params["projectIds"] = projectIDs
|
||||
|
||||
if milestoneTitle != "" {
|
||||
milestoneID, err := metadata.MilestoneToID(milestoneTitle)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add to milestone '%s': %w", milestoneTitle, err)
|
||||
}
|
||||
params["milestoneId"] = milestoneID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue) {
|
||||
table := utils.NewTablePrinter(w)
|
||||
for _, issue := range issues {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
|
@ -481,6 +482,102 @@ func TestIssueCreate(t *testing.T) {
|
|||
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
}
|
||||
|
||||
func TestIssueCreate_metadata(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bviewerPermission\b`),
|
||||
httpmock.StringResponse(httpmock.RepoNetworkStubResponse("OWNER", "REPO", "master", "WRITE")))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bhasIssuesEnabled\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID",
|
||||
"hasIssuesEnabled": true,
|
||||
"viewerPermission": "WRITE"
|
||||
} } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bassignableUsers\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID" },
|
||||
{ "login": "MonaLisa", "id": "MONAID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\blabels\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "labels": {
|
||||
"nodes": [
|
||||
{ "name": "feature", "id": "FEATUREID" },
|
||||
{ "name": "TODO", "id": "TODOID" },
|
||||
{ "name": "bug", "id": "BUGID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bmilestones\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "milestones": {
|
||||
"nodes": [
|
||||
{ "title": "GA", "id": "GAID" },
|
||||
{ "title": "Big One.oh", "id": "BIGONEID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\brepository\(.+\bprojects\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Cleanup", "id": "CLEANUPID" },
|
||||
{ "name": "Roadmap", "id": "ROADMAPID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\borganization\(.+\bprojects\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": null },
|
||||
"errors": [{
|
||||
"type": "NOT_FOUND",
|
||||
"path": [ "organization" ],
|
||||
"message": "Could not resolve to an Organization with the login of 'OWNER'."
|
||||
}]
|
||||
}
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bcreateIssue\(`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "createIssue": { "issue": {
|
||||
"URL": "https://github.com/OWNER/REPO/issues/12"
|
||||
} } } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["title"], "TITLE")
|
||||
eq(t, inputs["body"], "BODY")
|
||||
eq(t, inputs["assigneeIds"], []interface{}{"MONAID"})
|
||||
eq(t, inputs["labelIds"], []interface{}{"BUGID", "TODOID"})
|
||||
eq(t, inputs["projectIds"], []interface{}{"ROADMAPID"})
|
||||
eq(t, inputs["milestoneId"], "BIGONEID")
|
||||
}))
|
||||
|
||||
output, err := RunCommand(`issue create -t TITLE -b BODY -a monalisa -l bug -l todo -p roadmap -m 'big one.oh'`)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `issue create`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/issues/12\n")
|
||||
}
|
||||
|
||||
func TestIssueCreate_disabledIssues(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
|
|
|
|||
|
|
@ -124,6 +124,27 @@ func prCreate(cmd *cobra.Command, _ []string) error {
|
|||
return fmt.Errorf("could not parse body: %w", err)
|
||||
}
|
||||
|
||||
reviewers, err := cmd.Flags().GetStringSlice("reviewer")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse reviewers: %w", err)
|
||||
}
|
||||
assignees, err := cmd.Flags().GetStringSlice("assignee")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse assignees: %w", err)
|
||||
}
|
||||
labelNames, err := cmd.Flags().GetStringSlice("label")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse labels: %w", err)
|
||||
}
|
||||
projectNames, err := cmd.Flags().GetStringSlice("project")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse projects: %w", err)
|
||||
}
|
||||
milestoneTitle, err := cmd.Flags().GetString("milestone")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse milestone: %w", err)
|
||||
}
|
||||
|
||||
baseTrackingBranch := baseBranch
|
||||
if baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName()); err == nil {
|
||||
baseTrackingBranch = fmt.Sprintf("%s/%s", baseRemote.Name, baseBranch)
|
||||
|
|
@ -179,15 +200,24 @@ func prCreate(cmd *cobra.Command, _ []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: only drop into interactive mode if stdin & stdout are a tty
|
||||
if !isWeb && !autofill && (title == "" || body == "") {
|
||||
tb := issueMetadataState{
|
||||
Reviewers: reviewers,
|
||||
Assignees: assignees,
|
||||
Labels: labelNames,
|
||||
Projects: projectNames,
|
||||
Milestone: milestoneTitle,
|
||||
}
|
||||
|
||||
interactive := !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body"))
|
||||
|
||||
if !isWeb && !autofill && interactive {
|
||||
var templateFiles []string
|
||||
if rootDir, err := git.ToplevelDir(); err == nil {
|
||||
// TODO: figure out how to stub this in tests
|
||||
templateFiles = githubtemplate.Find(rootDir, "PULL_REQUEST_TEMPLATE")
|
||||
}
|
||||
|
||||
tb, err := titleBodySurvey(cmd, title, body, defs, templateFiles)
|
||||
err := titleBodySurvey(cmd, &tb, client, baseRepo, title, body, defs, templateFiles, true, baseRepo.ViewerCanTriage())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not collect title and/or body: %w", err)
|
||||
}
|
||||
|
|
@ -293,6 +323,51 @@ func prCreate(cmd *cobra.Command, _ []string) error {
|
|||
"headRefName": headBranchLabel,
|
||||
}
|
||||
|
||||
if tb.HasMetadata() {
|
||||
if tb.MetadataResult == nil {
|
||||
metadataInput := api.RepoMetadataInput{
|
||||
Reviewers: len(tb.Reviewers) > 0,
|
||||
Assignees: len(tb.Assignees) > 0,
|
||||
Labels: len(tb.Labels) > 0,
|
||||
Projects: len(tb.Projects) > 0,
|
||||
Milestones: tb.Milestone != "",
|
||||
}
|
||||
|
||||
// TODO: for non-interactive mode, only translate given objects to GraphQL IDs
|
||||
tb.MetadataResult, err = api.RepoMetadata(client, baseRepo, metadataInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = addMetadataToIssueParams(params, tb.MetadataResult, tb.Assignees, tb.Labels, tb.Projects, tb.Milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userReviewers []string
|
||||
var teamReviewers []string
|
||||
for _, r := range tb.Reviewers {
|
||||
if strings.ContainsRune(r, '/') {
|
||||
teamReviewers = append(teamReviewers, r)
|
||||
} else {
|
||||
userReviewers = append(teamReviewers, r)
|
||||
}
|
||||
}
|
||||
|
||||
userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not request reviewer: %w", err)
|
||||
}
|
||||
params["userReviewerIds"] = userReviewerIDs
|
||||
|
||||
teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not request reviewer: %w", err)
|
||||
}
|
||||
params["teamReviewerIds"] = teamReviewerIDs
|
||||
}
|
||||
|
||||
pr, err := api.CreatePullRequest(client, baseRepo, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create pull request: %w", err)
|
||||
|
|
@ -385,4 +460,10 @@ func init() {
|
|||
"The branch into which you want your code merged")
|
||||
prCreateCmd.Flags().BoolP("web", "w", false, "Open the web browser to create a pull request")
|
||||
prCreateCmd.Flags().BoolP("fill", "f", false, "Do not prompt for title/body and just use commit info")
|
||||
|
||||
prCreateCmd.Flags().StringSliceP("reviewer", "r", nil, "Request a review from someone by their `login`")
|
||||
prCreateCmd.Flags().StringSliceP("assignee", "a", nil, "Assign a person by their `login`")
|
||||
prCreateCmd.Flags().StringSliceP("label", "l", nil, "Add a label by `name`")
|
||||
prCreateCmd.Flags().StringSliceP("project", "p", nil, "Add the pull request to a project by `name`")
|
||||
prCreateCmd.Flags().StringP("milestone", "m", "", "Add the pull request to a milestone by `name`")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,141 @@ func TestPRCreate(t *testing.T) {
|
|||
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
|
||||
}
|
||||
|
||||
func TestPRCreate_metadata(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bviewerPermission\b`),
|
||||
httpmock.StringResponse(httpmock.RepoNetworkStubResponse("OWNER", "REPO", "master", "WRITE")))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bforks\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "forks": { "nodes": [
|
||||
] } } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bpullRequests\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
] } } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bassignableUsers\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID" },
|
||||
{ "login": "MonaLisa", "id": "MONAID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\blabels\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "labels": {
|
||||
"nodes": [
|
||||
{ "name": "feature", "id": "FEATUREID" },
|
||||
{ "name": "TODO", "id": "TODOID" },
|
||||
{ "name": "bug", "id": "BUGID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bmilestones\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "milestones": {
|
||||
"nodes": [
|
||||
{ "title": "GA", "id": "GAID" },
|
||||
{ "title": "Big One.oh", "id": "BIGONEID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\brepository\(.+\bprojects\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Cleanup", "id": "CLEANUPID" },
|
||||
{ "name": "Roadmap", "id": "ROADMAPID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\borganization\(.+\bprojects\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projects": {
|
||||
"nodes": [],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\borganization\(.+\bteams\(`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "teams": {
|
||||
"nodes": [
|
||||
{ "slug": "owners", "id": "OWNERSID" },
|
||||
{ "slug": "Core", "id": "COREID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bcreatePullRequest\(`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "createPullRequest": { "pullRequest": {
|
||||
"id": "NEWPULLID",
|
||||
"URL": "https://github.com/OWNER/REPO/pull/12"
|
||||
} } } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["title"], "TITLE")
|
||||
eq(t, inputs["body"], "BODY")
|
||||
}))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\bupdatePullRequest\(`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "updatePullRequest": {
|
||||
"clientMutationId": ""
|
||||
} } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["pullRequestId"], "NEWPULLID")
|
||||
eq(t, inputs["assigneeIds"], []interface{}{"MONAID"})
|
||||
eq(t, inputs["labelIds"], []interface{}{"BUGID", "TODOID"})
|
||||
eq(t, inputs["projectIds"], []interface{}{"ROADMAPID"})
|
||||
eq(t, inputs["milestoneId"], "BIGONEID")
|
||||
}))
|
||||
http.Register(
|
||||
httpmock.GraphQL(`\brequestReviews\(`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "requestReviews": {
|
||||
"clientMutationId": ""
|
||||
} } }
|
||||
`, func(inputs map[string]interface{}) {
|
||||
eq(t, inputs["pullRequestId"], "NEWPULLID")
|
||||
eq(t, inputs["userIds"], []interface{}{"HUBOTID"})
|
||||
eq(t, inputs["teamIds"], []interface{}{"COREID"})
|
||||
}))
|
||||
|
||||
cs, cmdTeardown := test.InitCmdStubber()
|
||||
defer cmdTeardown()
|
||||
|
||||
cs.Stub("") // git config --get-regexp (determineTrackingBranch)
|
||||
cs.Stub("") // git show-ref --verify (determineTrackingBranch)
|
||||
cs.Stub("") // git status
|
||||
cs.Stub("1234567890,commit 0\n2345678901,commit 1") // git log
|
||||
cs.Stub("") // git push
|
||||
|
||||
output, err := RunCommand(`pr create -t TITLE -b BODY -a monalisa -l bug -l todo -p roadmap -m 'big one.oh' -r hubot -r /core`)
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, output.String(), "https://github.com/OWNER/REPO/pull/12\n")
|
||||
}
|
||||
|
||||
func TestPRCreate_withForking(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
|
|
@ -374,7 +509,7 @@ func TestPRCreate_survey_defaults_multicommit(t *testing.T) {
|
|||
as.Stub([]*QuestionStub{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Value: 1,
|
||||
Value: 0,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -458,7 +593,7 @@ func TestPRCreate_survey_defaults_monocommit(t *testing.T) {
|
|||
as.Stub([]*QuestionStub{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Value: 1,
|
||||
Value: 0,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -602,7 +737,7 @@ func TestPRCreate_defaults_error_interactive(t *testing.T) {
|
|||
as.Stub([]*QuestionStub{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Value: 0,
|
||||
Value: 1,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -5,30 +5,67 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/githubtemplate"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Action int
|
||||
|
||||
type titleBody struct {
|
||||
type issueMetadataState struct {
|
||||
Body string
|
||||
Title string
|
||||
Action Action
|
||||
|
||||
Metadata []string
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestone string
|
||||
|
||||
MetadataResult *api.RepoMetadataResult
|
||||
}
|
||||
|
||||
func (tb *issueMetadataState) HasMetadata() bool {
|
||||
return len(tb.Reviewers) > 0 ||
|
||||
len(tb.Assignees) > 0 ||
|
||||
len(tb.Labels) > 0 ||
|
||||
len(tb.Projects) > 0 ||
|
||||
tb.Milestone != ""
|
||||
}
|
||||
|
||||
const (
|
||||
PreviewAction Action = iota
|
||||
SubmitAction
|
||||
SubmitAction Action = iota
|
||||
PreviewAction
|
||||
CancelAction
|
||||
MetadataAction
|
||||
)
|
||||
|
||||
var SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
|
||||
return survey.Ask(qs, response, opts...)
|
||||
}
|
||||
|
||||
func confirmSubmission() (Action, error) {
|
||||
func confirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) {
|
||||
const (
|
||||
submitLabel = "Submit"
|
||||
previewLabel = "Continue in browser"
|
||||
metadataLabel = "Add metadata"
|
||||
cancelLabel = "Cancel"
|
||||
)
|
||||
|
||||
options := []string{submitLabel}
|
||||
if allowPreview {
|
||||
options = append(options, previewLabel)
|
||||
}
|
||||
if allowMetadata {
|
||||
options = append(options, metadataLabel)
|
||||
}
|
||||
options = append(options, cancelLabel)
|
||||
|
||||
confirmAnswers := struct {
|
||||
Confirmation int
|
||||
}{}
|
||||
|
|
@ -37,11 +74,7 @@ func confirmSubmission() (Action, error) {
|
|||
Name: "confirmation",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What's next?",
|
||||
Options: []string{
|
||||
"Preview in browser",
|
||||
"Submit",
|
||||
"Cancel",
|
||||
},
|
||||
Options: options,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -51,7 +84,18 @@ func confirmSubmission() (Action, error) {
|
|||
return -1, fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
return Action(confirmAnswers.Confirmation), nil
|
||||
switch options[confirmAnswers.Confirmation] {
|
||||
case submitLabel:
|
||||
return SubmitAction, nil
|
||||
case previewLabel:
|
||||
return PreviewAction, nil
|
||||
case metadataLabel:
|
||||
return MetadataAction, nil
|
||||
case cancelLabel:
|
||||
return CancelAction, nil
|
||||
default:
|
||||
return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation)
|
||||
}
|
||||
}
|
||||
|
||||
func selectTemplate(templatePaths []string) (string, error) {
|
||||
|
|
@ -82,19 +126,18 @@ func selectTemplate(templatePaths []string) (string, error) {
|
|||
return string(templateContents), nil
|
||||
}
|
||||
|
||||
func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, defs defaults, templatePaths []string) (*titleBody, error) {
|
||||
func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs defaults, templatePaths []string, allowReviewers, allowMetadata bool) error {
|
||||
editorCommand := os.Getenv("GH_EDITOR")
|
||||
if editorCommand == "" {
|
||||
ctx := contextForCommand(cmd)
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read config: %w", err)
|
||||
return fmt.Errorf("could not read config: %w", err)
|
||||
}
|
||||
editorCommand, _ = cfg.Get(defaultHostname, "editor")
|
||||
}
|
||||
|
||||
var inProgress titleBody
|
||||
inProgress.Title = defs.Title
|
||||
issueState.Title = defs.Title
|
||||
templateContents := ""
|
||||
|
||||
if providedBody == "" {
|
||||
|
|
@ -102,11 +145,11 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def
|
|||
var err error
|
||||
templateContents, err = selectTemplate(templatePaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
inProgress.Body = templateContents
|
||||
issueState.Body = templateContents
|
||||
} else {
|
||||
inProgress.Body = defs.Body
|
||||
issueState.Body = defs.Body
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +157,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def
|
|||
Name: "title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Title",
|
||||
Default: inProgress.Title,
|
||||
Default: issueState.Title,
|
||||
},
|
||||
}
|
||||
bodyQuestion := &survey.Question{
|
||||
|
|
@ -124,7 +167,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def
|
|||
Editor: &survey.Editor{
|
||||
Message: "Body",
|
||||
FileName: "*.md",
|
||||
Default: inProgress.Body,
|
||||
Default: issueState.Body,
|
||||
HideDefault: true,
|
||||
AppendDefault: true,
|
||||
},
|
||||
|
|
@ -139,21 +182,175 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def
|
|||
qs = append(qs, bodyQuestion)
|
||||
}
|
||||
|
||||
err := SurveyAsk(qs, &inProgress)
|
||||
err := SurveyAsk(qs, issueState)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not prompt: %w", err)
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if inProgress.Body == "" {
|
||||
inProgress.Body = templateContents
|
||||
if issueState.Body == "" {
|
||||
issueState.Body = templateContents
|
||||
}
|
||||
|
||||
confirmA, err := confirmSubmission()
|
||||
allowPreview := !issueState.HasMetadata()
|
||||
confirmA, err := confirmSubmission(allowPreview, allowMetadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to confirm: %w", err)
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
|
||||
inProgress.Action = confirmA
|
||||
if confirmA == MetadataAction {
|
||||
isChosen := func(m string) bool {
|
||||
for _, c := range issueState.Metadata {
|
||||
if m == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return &inProgress, nil
|
||||
extraFieldsOptions := []string{}
|
||||
if allowReviewers {
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Reviewers")
|
||||
}
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
|
||||
|
||||
err = SurveyAsk([]*survey.Question{
|
||||
{
|
||||
Name: "metadata",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "What would you like to add?",
|
||||
Options: extraFieldsOptions,
|
||||
},
|
||||
},
|
||||
}, issueState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
metadataInput := api.RepoMetadataInput{
|
||||
Reviewers: isChosen("Reviewers"),
|
||||
Assignees: isChosen("Assignees"),
|
||||
Labels: isChosen("Labels"),
|
||||
Projects: isChosen("Projects"),
|
||||
Milestones: isChosen("Milestone"),
|
||||
}
|
||||
s := utils.Spinner(cmd.OutOrStderr())
|
||||
utils.StartSpinner(s)
|
||||
issueState.MetadataResult, err = api.RepoMetadata(apiClient, repo, metadataInput)
|
||||
utils.StopSpinner(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching metadata options: %w", err)
|
||||
}
|
||||
|
||||
var users []string
|
||||
for _, u := range issueState.MetadataResult.AssignableUsers {
|
||||
users = append(users, u.Login)
|
||||
}
|
||||
var teams []string
|
||||
for _, t := range issueState.MetadataResult.Teams {
|
||||
teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug))
|
||||
}
|
||||
var labels []string
|
||||
for _, l := range issueState.MetadataResult.Labels {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
var projects []string
|
||||
for _, l := range issueState.MetadataResult.Projects {
|
||||
projects = append(projects, l.Name)
|
||||
}
|
||||
milestones := []string{"(none)"}
|
||||
for _, m := range issueState.MetadataResult.Milestones {
|
||||
milestones = append(milestones, m.Title)
|
||||
}
|
||||
|
||||
var mqs []*survey.Question
|
||||
if isChosen("Reviewers") {
|
||||
if len(users) > 0 || len(teams) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "reviewers",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Reviewers",
|
||||
Options: append(users, teams...),
|
||||
Default: issueState.Reviewers,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no available reviewers")
|
||||
}
|
||||
}
|
||||
if isChosen("Assignees") {
|
||||
if len(users) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "assignees",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Assignees",
|
||||
Options: users,
|
||||
Default: issueState.Assignees,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no assignable users")
|
||||
}
|
||||
}
|
||||
if isChosen("Labels") {
|
||||
if len(labels) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "labels",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Labels",
|
||||
Options: labels,
|
||||
Default: issueState.Labels,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no labels in the repository")
|
||||
}
|
||||
}
|
||||
if isChosen("Projects") {
|
||||
if len(projects) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "projects",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Projects",
|
||||
Options: projects,
|
||||
Default: issueState.Projects,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no projects to choose from")
|
||||
}
|
||||
}
|
||||
if isChosen("Milestone") {
|
||||
if len(milestones) > 1 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "milestone",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Milestone",
|
||||
Options: milestones,
|
||||
Default: issueState.Milestone,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no milestones in the repository")
|
||||
}
|
||||
}
|
||||
|
||||
err = SurveyAsk(mqs, issueState, survey.WithKeepFilter(true))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if issueState.Milestone == "(none)" {
|
||||
issueState.Milestone = ""
|
||||
}
|
||||
|
||||
allowPreview = !issueState.HasMetadata()
|
||||
allowMetadata = false
|
||||
confirmA, err = confirmSubmission(allowPreview, allowMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
issueState.Action = confirmA
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue