From d3a89b8744047371bb96ecdae1f9c146fe4a58e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 17 Apr 2020 20:23:57 +0200 Subject: [PATCH] Expand `issue create` metadata flags to `pr create` - Includes support `pr create --reviewer ` - Hide "Preview in browser" menu option when any metadata are set --- api/queries_pr.go | 66 ++++++++++++++++++++++- api/queries_repo.go | 63 ++++++++++++++++++++++ command/issue.go | 100 +++++++++++++---------------------- command/pr_create.go | 47 +++++++++++++++- command/title_body_survey.go | 24 +++++---- 5 files changed, 225 insertions(+), 75 deletions(-) diff --git a/api/queries_pr.go b/api/queries_pr.go index 0b2fd378a..922896b27 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -20,6 +20,7 @@ type PullRequestAndTotalCount struct { } type PullRequest struct { + ID string Number int Title string State string @@ -558,6 +559,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter mutation CreatePullRequest($input: CreatePullRequestInput!) { createPullRequest(input: $input) { pullRequest { + id url } } @@ -567,7 +569,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, @@ -583,8 +588,65 @@ 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: CreatePullRequestInput!) { + updatePullRequest(input: $input) + }` + updateParams["pullRequestId"] = pr.ID + variables := map[string]interface{}{ + "input": inputParams, + } + err := client.GraphQL(updateQuery, variables, &result) + if err != nil { + return nil, err + } + } + + // reviewers are requested in yet another additional mutation + if ids, ok := params["reviewerIds"]; ok && !isBlank(ids) { + reviewQuery := ` + mutation RequestReviews($input: CreatePullRequestInput!) { + requestReviews(input: $input) + }` + reviewParams := map[string]interface{}{ + "pullRequestId": pr.ID, + "userIds": ids, + } + 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 vv != nil && len(vv) > 0 + default: + return true + } } func PullRequestList(client *Client, vars map[string]interface{}, limit int) (*PullRequestAndTotalCount, error) { diff --git a/api/queries_repo.go b/api/queries_repo.go index abd96cbaf..6168a4573 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -415,6 +415,69 @@ type RepoMetadataResult struct { } `graphql:"milestones(first: 100, states: [OPEN])"` } +func (m *RepoMetadataResult) AssigneesToIDs(names []string) ([]string, error) { + var ids []string + for _, assigneeLogin := range names { + found := false + for _, u := range m.AssignableUsers.Nodes { + 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) LabelsToIDs(names []string) ([]string, error) { + var ids []string + for _, labelName := range names { + found := false + for _, l := range m.Labels.Nodes { + 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.Nodes { + 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.Nodes { + if strings.EqualFold(title, m.Title) { + return m.ID, nil + } + } + return "", errors.New("not found") +} + // RepoMetadata pre-fetches the metadata for attaching to issues and pull requests func RepoMetadata(client *Client, repo ghrepo.Interface) (*RepoMetadataResult, error) { var query struct { diff --git a/command/issue.go b/command/issue.go index 7236297a0..9e09d8a0e 100644 --- a/command/issue.go +++ b/command/issue.go @@ -371,6 +371,8 @@ func issueCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("could not parse milestone: %w", err) } + hasMetadata := len(assignees) > 0 || len(labelNames) > 0 || len(projectNames) > 0 || milestoneTitle != "" + 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)) @@ -407,7 +409,7 @@ func issueCreate(cmd *cobra.Command, args []string) error { interactive := title == "" || body == "" if interactive { - tb, err := titleBodySurvey(cmd, title, body, defaults{}, templateFiles) + tb, err := titleBodySurvey(cmd, title, body, defaults{}, templateFiles, !hasMetadata) if err != nil { return fmt.Errorf("could not collect title and/or body: %w", err) } @@ -444,72 +446,14 @@ func issueCreate(cmd *cobra.Command, args []string) error { "body": body, } - if len(assignees) > 0 || len(labelNames) > 0 || len(projectNames) > 0 || milestoneTitle != "" { + if hasMetadata { metadata, err := api.RepoMetadata(apiClient, baseRepo) if err != nil { return err } - - var assigneeIDs []string - for _, assigneeLogin := range assignees { - var found bool - for _, u := range metadata.AssignableUsers.Nodes { - if strings.EqualFold(assigneeLogin, u.Login) { - assigneeIDs = append(assigneeIDs, u.ID) - found = true - break - } - } - if !found { - return fmt.Errorf("could not find user to assign: '%s'", assigneeLogin) - } - } - params["assigneeIds"] = assigneeIDs - - var labelIDs []string - for _, labelName := range labelNames { - var found bool - for _, l := range metadata.Labels.Nodes { - if strings.EqualFold(labelName, l.Name) { - labelIDs = append(labelIDs, l.ID) - found = true - break - } - } - if !found { - return fmt.Errorf("could not find label '%s'", labelName) - } - } - params["labelIds"] = labelIDs - - var projectIDs []string - for _, projectName := range projectNames { - var found bool - for _, p := range metadata.Projects.Nodes { - if strings.EqualFold(projectName, p.Name) { - projectIDs = append(projectIDs, p.ID) - found = true - break - } - } - if !found { - return fmt.Errorf("could not find project '%s'", projectName) - } - } - params["projectIds"] = projectIDs - - if milestoneTitle != "" { - var milestoneID string - for _, m := range metadata.Milestones.Nodes { - if strings.EqualFold(milestoneTitle, m.Title) { - milestoneID = m.ID - break - } - } - if milestoneID == "" { - return fmt.Errorf("could not find milestone '%s'", milestoneTitle) - } - params["milestoneId"] = milestoneID + err = addMetadataToIssueParams(params, metadata, assignees, labelNames, projectNames, milestoneTitle) + if err != nil { + return err } } @@ -526,6 +470,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.AssigneesToIDs(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 { diff --git a/command/pr_create.go b/command/pr_create.go index 96b3e27df..0b4ffa042 100644 --- a/command/pr_create.go +++ b/command/pr_create.go @@ -124,6 +124,29 @@ 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) + } + + hasMetadata := len(reviewers) > 0 || len(assignees) > 0 || len(labelNames) > 0 || len(projectNames) > 0 || milestoneTitle != "" + baseTrackingBranch := baseBranch if baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName()); err == nil { baseTrackingBranch = fmt.Sprintf("%s/%s", baseRemote.Name, baseBranch) @@ -187,7 +210,7 @@ func prCreate(cmd *cobra.Command, _ []string) error { templateFiles = githubtemplate.Find(rootDir, "PULL_REQUEST_TEMPLATE") } - tb, err := titleBodySurvey(cmd, title, body, defs, templateFiles) + tb, err := titleBodySurvey(cmd, title, body, defs, templateFiles, !hasMetadata) if err != nil { return fmt.Errorf("could not collect title and/or body: %w", err) } @@ -293,6 +316,22 @@ func prCreate(cmd *cobra.Command, _ []string) error { "headRefName": headBranchLabel, } + if hasMetadata { + metadata, err := api.RepoMetadata(client, baseRepo) + if err != nil { + return err + } + err = addMetadataToIssueParams(params, metadata, assignees, labelNames, projectNames, milestoneTitle) + if err != nil { + return err + } + reviewerIDs, err := metadata.AssigneesToIDs(reviewers) + if err != nil { + return fmt.Errorf("could not request reviewer: %w", err) + } + params["reviewerIds"] = reviewerIDs + } + pr, err := api.CreatePullRequest(client, baseRepo, params) if err != nil { return fmt.Errorf("failed to create pull request: %w", err) @@ -385,4 +424,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`") } diff --git a/command/title_body_survey.go b/command/title_body_survey.go index 8d4dd4d47..27b6c9687 100644 --- a/command/title_body_survey.go +++ b/command/title_body_survey.go @@ -27,7 +27,13 @@ var SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey return survey.Ask(qs, response, opts...) } -func confirmSubmission() (Action, error) { +func confirmSubmission(allowPreview bool) (Action, error) { + options := []string{} + if allowPreview { + options = append(options, "Preview in browser") + } + options = append(options, "Submit", "Cancel") + confirmAnswers := struct { Confirmation int }{} @@ -36,11 +42,7 @@ func confirmSubmission() (Action, error) { Name: "confirmation", Prompt: &survey.Select{ Message: "What's next?", - Options: []string{ - "Preview in browser", - "Submit", - "Cancel", - }, + Options: options, }, }, } @@ -50,7 +52,11 @@ func confirmSubmission() (Action, error) { return -1, fmt.Errorf("could not prompt: %w", err) } - return Action(confirmAnswers.Confirmation), nil + choice := confirmAnswers.Confirmation + if !allowPreview { + choice++ + } + return Action(choice), nil } func selectTemplate(templatePaths []string) (string, error) { @@ -81,7 +87,7 @@ 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, providedTitle, providedBody string, defs defaults, templatePaths []string, allowPreview bool) (*titleBody, error) { var inProgress titleBody inProgress.Title = defs.Title templateContents := "" @@ -136,7 +142,7 @@ func titleBodySurvey(cmd *cobra.Command, providedTitle, providedBody string, def inProgress.Body = templateContents } - confirmA, err := confirmSubmission() + confirmA, err := confirmSubmission(allowPreview) if err != nil { return nil, fmt.Errorf("unable to confirm: %w", err) }