Change behavior of slice flags for issue edit and pr edit commands

This commit is contained in:
Sam Coe 2021-02-10 15:13:59 -08:00
parent a84145eb68
commit 4fdf28d8a4
No known key found for this signature in database
GPG key ID: 8E322C20F811D086
9 changed files with 651 additions and 407 deletions

View file

@ -49,6 +49,14 @@ type Assignees struct {
TotalCount int
}
func (a Assignees) Logins() []string {
logins := make([]string, len(a.Nodes))
for i, a := range a.Nodes {
logins[i] = a.Login
}
return logins
}
type Labels struct {
Nodes []struct {
Name string
@ -56,6 +64,14 @@ type Labels struct {
TotalCount int
}
func (l Labels) Names() []string {
names := make([]string, len(l.Nodes))
for i, l := range l.Nodes {
names[i] = l.Name
}
return names
}
type ProjectCards struct {
Nodes []struct {
Project struct {
@ -68,6 +84,14 @@ type ProjectCards struct {
TotalCount int
}
func (p ProjectCards) ProjectNames() []string {
names := make([]string, len(p.Nodes))
for i, c := range p.Nodes {
names[i] = c.Project.Name
}
return names
}
type Milestone struct {
Title string
}

View file

@ -80,32 +80,10 @@ type PullRequest struct {
}
}
}
Assignees struct {
Nodes []struct {
Login string
}
TotalCount int
}
Labels struct {
Nodes []struct {
Name string
}
TotalCount int
}
ProjectCards struct {
Nodes []struct {
Project struct {
Name string
}
Column struct {
Name string
}
}
TotalCount int
}
Milestone struct {
Title string
}
Assignees Assignees
Labels Labels
ProjectCards ProjectCards
Milestone Milestone
Comments Comments
ReactionGroups ReactionGroups
Reviews PullRequestReviews
@ -123,6 +101,14 @@ type ReviewRequests struct {
TotalCount int
}
func (r ReviewRequests) Logins() []string {
logins := make([]string, len(r.Nodes))
for i, a := range r.Nodes {
logins[i] = a.RequestedReviewer.Login
}
return logins
}
type NotFoundError struct {
error
}
@ -816,6 +802,7 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
reviewParams["teamIds"] = ids
}
//TODO: How much work to extract this into own method and use for create and edit?
if len(reviewParams) > 0 {
reviewQuery := `
mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) {

View file

@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
@ -497,7 +496,7 @@ func (m *RepoMetadataResult) MilestoneToID(title string) (string, error) {
return m.ID, nil
}
}
return "", errors.New("not found")
return "", fmt.Errorf("'%s' not found", title)
}
func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) {

View file

@ -47,11 +47,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
Short: "Edit an issue",
Example: heredoc.Doc(`
$ gh issue edit 23 --title "I found a bug" --body "Nothing works"
$ gh issue edit 23 --label "bug,help wanted"
$ gh issue edit 23 --label bug --label "help wanted"
$ gh issue edit 23 --assignee monalisa,hubot
$ gh issue edit 23 --assignee @me
$ gh issue edit 23 --project "Roadmap"
$ gh issue edit 23 --add-label "bug,help wanted" --remove-label "core"
$ gh issue edit 23 --add-assignee @me --remove-assignee monalisa,hubot
$ gh issue edit 23 --add-project "Roadmap" --remove-project v1,v2
$ gh issue edit 23 --milestone "Version 1"
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@ -62,22 +61,22 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
flags := cmd.Flags()
if flags.Changed("title") {
opts.Editable.TitleEdited = true
opts.Editable.Title.Edited = true
}
if flags.Changed("body") {
opts.Editable.BodyEdited = true
opts.Editable.Body.Edited = true
}
if flags.Changed("assignee") {
opts.Editable.AssigneesEdited = true
if flags.Changed("add-assignee") || flags.Changed("remove-assignee") {
opts.Editable.Assignees.Edited = true
}
if flags.Changed("label") {
opts.Editable.LabelsEdited = true
if flags.Changed("add-label") || flags.Changed("remove-label") {
opts.Editable.Labels.Edited = true
}
if flags.Changed("project") {
opts.Editable.ProjectsEdited = true
if flags.Changed("add-project") || flags.Changed("remove-project") {
opts.Editable.Projects.Edited = true
}
if flags.Changed("milestone") {
opts.Editable.MilestoneEdited = true
opts.Editable.Milestone.Edited = true
}
if !opts.Editable.Dirty() {
@ -85,7 +84,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
}
if opts.Interactive && !opts.IO.CanPrompt() {
return &cmdutil.FlagError{Err: errors.New("--tile, --body, --assignee, --label, --project, or --milestone required when not running interactively")}
return &cmdutil.FlagError{Err: errors.New("field to edit flag required when not running interactively")}
}
if runF != nil {
@ -96,12 +95,15 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
},
}
cmd.Flags().StringVarP(&opts.Editable.Title, "title", "t", "", "Revise the issue title.")
cmd.Flags().StringVarP(&opts.Editable.Body, "body", "b", "", "Revise the issue body.")
cmd.Flags().StringSliceVarP(&opts.Editable.Assignees, "assignee", "a", nil, "Set assigned people by their `login`. Use \"@me\" to self-assign.")
cmd.Flags().StringSliceVarP(&opts.Editable.Labels, "label", "l", nil, "Set the issue labels by `name`")
cmd.Flags().StringSliceVarP(&opts.Editable.Projects, "project", "p", nil, "Set the projects the issue belongs to by `name`")
cmd.Flags().StringVarP(&opts.Editable.Milestone, "milestone", "m", "", "Set the milestone the issue belongs to by `name`")
cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself.")
cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Remove, "remove-assignee", nil, "Remove assigned users by their `login`. Use \"@me\" to unassign yourself.")
cmd.Flags().StringSliceVar(&opts.Editable.Labels.Add, "add-label", nil, "Add labels by `name`")
cmd.Flags().StringSliceVar(&opts.Editable.Labels.Remove, "remove-label", nil, "Remove labels by `name`")
cmd.Flags().StringSliceVar(&opts.Editable.Projects.Add, "add-project", nil, "Add the issue to projects by `name`")
cmd.Flags().StringSliceVar(&opts.Editable.Projects.Remove, "remove-project", nil, "Remove the issue from projects by `name`")
cmd.Flags().StringVarP(&opts.Editable.Milestone.Value, "milestone", "m", "", "Edit the milestone the issue belongs to by `name`")
return cmd
}
@ -119,12 +121,12 @@ func editRun(opts *EditOptions) error {
}
editable := opts.Editable
editable.TitleDefault = issue.Title
editable.BodyDefault = issue.Body
editable.AssigneesDefault = issue.Assignees
editable.LabelsDefault = issue.Labels
editable.ProjectsDefault = issue.ProjectCards
editable.MilestoneDefault = issue.Milestone
editable.Title.Default = issue.Title
editable.Body.Default = issue.Body
editable.Assignees.Default = issue.Assignees.Logins()
editable.Labels.Default = issue.Labels.Names()
editable.Projects.Default = issue.ProjectCards.ProjectNames()
editable.Milestone.Default = issue.Milestone.Title
if opts.Interactive {
err = opts.FieldsToEditSurvey(&editable)
@ -167,24 +169,59 @@ func updateIssue(client *api.Client, repo ghrepo.Interface, id string, options p
var err error
params := githubv4.UpdateIssueInput{
ID: id,
Title: options.TitleParam(),
Body: options.BodyParam(),
Title: ghString(options.TitleValue()),
Body: ghString(options.BodyValue()),
}
params.AssigneeIDs, err = options.AssigneesParam(client, repo)
assigneeIds, err := options.AssigneeIds(client, repo)
if err != nil {
return err
}
params.LabelIDs, err = options.LabelsParam()
params.AssigneeIDs = ghIds(assigneeIds)
labelIds, err := options.LabelIds()
if err != nil {
return err
}
params.ProjectIDs, err = options.ProjectsParam()
params.LabelIDs = ghIds(labelIds)
projectIds, err := options.ProjectIds()
if err != nil {
return err
}
params.MilestoneID, err = options.MilestoneParam()
params.ProjectIDs = ghIds(projectIds)
milestoneId, err := options.MilestoneId()
if err != nil {
return err
}
params.MilestoneID = ghId(milestoneId)
return api.IssueUpdate(client, repo, params)
}
func ghIds(s *[]string) *[]githubv4.ID {
if s == nil {
return nil
}
ids := make([]githubv4.ID, len(*s))
for i, v := range *s {
ids[i] = v
}
return &ids
}
func ghId(s *string) *githubv4.ID {
if s == nil {
return nil
}
if *s == "" {
r := githubv4.ID(nil)
return &r
}
r := githubv4.ID(*s)
return &r
}
func ghString(s *string) *githubv4.String {
if s == nil {
return nil
}
r := githubv4.String(*s)
return &r
}

View file

@ -42,8 +42,10 @@ func TestNewCmdEdit(t *testing.T) {
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Title: "test",
TitleEdited: true,
Title: prShared.EditableString{
Value: "test",
Edited: true,
},
},
},
wantsErr: false,
@ -54,44 +56,94 @@ func TestNewCmdEdit(t *testing.T) {
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Body: "test",
BodyEdited: true,
Body: prShared.EditableString{
Value: "test",
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "assignee flag",
input: "23 --assignee monalisa,hubot",
name: "add-assignee flag",
input: "23 --add-assignee monalisa,hubot",
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Assignees: []string{"monalisa", "hubot"},
AssigneesEdited: true,
Assignees: prShared.EditableSlice{
Add: []string{"monalisa", "hubot"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "label flag",
input: "23 --label feature,TODO,bug",
name: "remove-assignee flag",
input: "23 --remove-assignee monalisa,hubot",
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Labels: []string{"feature", "TODO", "bug"},
LabelsEdited: true,
Assignees: prShared.EditableSlice{
Remove: []string{"monalisa", "hubot"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "project flag",
input: "23 --project Cleanup,Roadmap",
name: "add-label flag",
input: "23 --add-label feature,TODO,bug",
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Projects: []string{"Cleanup", "Roadmap"},
ProjectsEdited: true,
Labels: prShared.EditableSlice{
Add: []string{"feature", "TODO", "bug"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "remove-label flag",
input: "23 --remove-label feature,TODO,bug",
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Labels: prShared.EditableSlice{
Remove: []string{"feature", "TODO", "bug"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "add-project flag",
input: "23 --add-project Cleanup,Roadmap",
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Projects: prShared.EditableSlice{
Add: []string{"Cleanup", "Roadmap"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "remove-project flag",
input: "23 --remove-project Cleanup,Roadmap",
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Projects: prShared.EditableSlice{
Remove: []string{"Cleanup", "Roadmap"},
Edited: true,
},
},
},
wantsErr: false,
@ -102,8 +154,10 @@ func TestNewCmdEdit(t *testing.T) {
output: EditOptions{
SelectorArg: "23",
Editable: prShared.Editable{
Milestone: "GA",
MilestoneEdited: true,
Milestone: prShared.EditableString{
Value: "GA",
Edited: true,
},
},
},
wantsErr: false,
@ -163,18 +217,33 @@ func Test_editRun(t *testing.T) {
SelectorArg: "123",
Interactive: false,
Editable: prShared.Editable{
Title: "new title",
TitleEdited: true,
Body: "new body",
BodyEdited: true,
Assignees: []string{"monalisa", "hubot"},
AssigneesEdited: true,
Labels: []string{"feature", "TODO", "bug"},
LabelsEdited: true,
Projects: []string{"Cleanup", "Roadmap"},
ProjectsEdited: true,
Milestone: "GA",
MilestoneEdited: true,
Title: prShared.EditableString{
Value: "new title",
Edited: true,
},
Body: prShared.EditableString{
Value: "new body",
Edited: true,
},
Assignees: prShared.EditableSlice{
Add: []string{"monalisa", "hubot"},
Remove: []string{"octocat"},
Edited: true,
},
Labels: prShared.EditableSlice{
Add: []string{"feature", "TODO", "bug"},
Remove: []string{"docs"},
Edited: true,
},
Projects: prShared.EditableSlice{
Add: []string{"Cleanup", "Roadmap"},
Remove: []string{"Features"},
Edited: true,
},
Milestone: prShared.EditableString{
Value: "GA",
Edited: true,
},
},
FetchOptions: prShared.FetchOptions,
},
@ -191,21 +260,21 @@ func Test_editRun(t *testing.T) {
SelectorArg: "123",
Interactive: true,
FieldsToEditSurvey: func(eo *prShared.Editable) error {
eo.TitleEdited = true
eo.BodyEdited = true
eo.AssigneesEdited = true
eo.LabelsEdited = true
eo.ProjectsEdited = true
eo.MilestoneEdited = true
eo.Title.Edited = true
eo.Body.Edited = true
eo.Assignees.Edited = true
eo.Labels.Edited = true
eo.Projects.Edited = true
eo.Milestone.Edited = true
return nil
},
EditFieldsSurvey: func(eo *prShared.Editable, _ string) error {
eo.Title = "new title"
eo.Body = "new body"
eo.Assignees = []string{"monalisa", "hubot"}
eo.Labels = []string{"feature", "TODO", "bug"}
eo.Projects = []string{"Cleanup", "Roadmap"}
eo.Milestone = "GA"
eo.Title.Value = "new title"
eo.Body.Value = "new body"
eo.Assignees.Value = []string{"monalisa", "hubot"}
eo.Labels.Value = []string{"feature", "TODO", "bug"}
eo.Projects.Value = []string{"Cleanup", "Roadmap"}
eo.Milestone.Value = "GA"
return nil
},
FetchOptions: prShared.FetchOptions,

View file

@ -50,12 +50,11 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
Short: "Edit a pull request",
Example: heredoc.Doc(`
$ gh pr edit 23 --title "I found a bug" --body "Nothing works"
$ gh pr edit 23 --label "bug,help wanted"
$ gh pr edit 23 --label bug --label "help wanted"
$ gh pr edit 23 --reviewer monalisa,hubot --reviewer myorg/team-name
$ gh pr edit 23 --assignee monalisa,hubot
$ gh pr edit 23 --assignee @me
$ gh pr edit 23 --project "Roadmap"
$ gh pr edit 23 --add-label "bug,help wanted" --remove-label "core"
$ gh pr edit 23 --add-reviewer monalisa,hubot --remove-reviewer myorg/team-name
$ gh pr edit 23 --add-assignee @me --remove-assignee monalisa,hubot
$ gh pr edit 23 --add-project "Roadmap" --remove-project v1,v2
$ gh pr edit 23 --milestone "Version 1"
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@ -66,25 +65,25 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
flags := cmd.Flags()
if flags.Changed("title") {
opts.Editable.TitleEdited = true
opts.Editable.Title.Edited = true
}
if flags.Changed("body") {
opts.Editable.BodyEdited = true
opts.Editable.Body.Edited = true
}
if flags.Changed("reviewer") {
opts.Editable.ReviewersEdited = true
if flags.Changed("add-reviewer") || flags.Changed("remove-reviewer") {
opts.Editable.Reviewers.Edited = true
}
if flags.Changed("assignee") {
opts.Editable.AssigneesEdited = true
if flags.Changed("add-assignee") || flags.Changed("remove-assignee") {
opts.Editable.Assignees.Edited = true
}
if flags.Changed("label") {
opts.Editable.LabelsEdited = true
if flags.Changed("add-label") || flags.Changed("remove-label") {
opts.Editable.Labels.Edited = true
}
if flags.Changed("project") {
opts.Editable.ProjectsEdited = true
if flags.Changed("add-project") || flags.Changed("remove-project") {
opts.Editable.Projects.Edited = true
}
if flags.Changed("milestone") {
opts.Editable.MilestoneEdited = true
opts.Editable.Milestone.Edited = true
}
if !opts.Editable.Dirty() {
@ -103,13 +102,17 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
},
}
cmd.Flags().StringVarP(&opts.Editable.Title, "title", "t", "", "Revise the pr title.")
cmd.Flags().StringVarP(&opts.Editable.Body, "body", "b", "", "Revise the pr body.")
cmd.Flags().StringSliceVarP(&opts.Editable.Reviewers, "reviewer", "r", nil, "Request reviews from people or teams by their `handle`")
cmd.Flags().StringSliceVarP(&opts.Editable.Assignees, "assignee", "a", nil, "Set assigned people by their `login`. Use \"@me\" to self-assign.")
cmd.Flags().StringSliceVarP(&opts.Editable.Labels, "label", "l", nil, "Set the pr labels by `name`")
cmd.Flags().StringSliceVarP(&opts.Editable.Projects, "project", "p", nil, "Set the projects the pr belongs to by `name`")
cmd.Flags().StringVarP(&opts.Editable.Milestone, "milestone", "m", "", "Set the milestone the pr belongs to by `name`")
cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Add, "add-reviewer", nil, "Add reviewers by their `login`.")
cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Remove, "remove-reviewer", nil, "Remove reviewers by their `login`.")
cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself.")
cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Remove, "remove-assignee", nil, "Remove assigned users by their `login`. Use \"@me\" to unassign yourself.")
cmd.Flags().StringSliceVar(&opts.Editable.Labels.Add, "add-label", nil, "Add labels by `name`")
cmd.Flags().StringSliceVar(&opts.Editable.Labels.Remove, "remove-label", nil, "Remove labels by `name`")
cmd.Flags().StringSliceVar(&opts.Editable.Projects.Add, "add-project", nil, "Add the pull request to projects by `name`")
cmd.Flags().StringSliceVar(&opts.Editable.Projects.Remove, "remove-project", nil, "Remove the pull request from projects by `name`")
cmd.Flags().StringVarP(&opts.Editable.Milestone.Value, "milestone", "m", "", "Edit the milestone the pull request belongs to by `name`")
return cmd
}
@ -127,14 +130,14 @@ func editRun(opts *EditOptions) error {
}
editable := opts.Editable
editable.ReviewersAllowed = true
editable.TitleDefault = pr.Title
editable.BodyDefault = pr.Body
editable.ReviewersDefault = pr.ReviewRequests
editable.AssigneesDefault = pr.Assignees
editable.LabelsDefault = pr.Labels
editable.ProjectsDefault = pr.ProjectCards
editable.MilestoneDefault = pr.Milestone
editable.Reviewers.Allowed = true
editable.Title.Default = pr.Title
editable.Body.Default = pr.Body
editable.Reviewers.Default = pr.ReviewRequests.Logins()
editable.Assignees.Default = pr.Assignees.Logins()
editable.Labels.Default = pr.Labels.Names()
editable.Projects.Default = pr.ProjectCards.ProjectNames()
editable.Milestone.Default = pr.Milestone.Title
if opts.Interactive {
err = opts.Surveyor.FieldsToEdit(&editable)
@ -177,25 +180,29 @@ func updatePullRequest(client *api.Client, repo ghrepo.Interface, id string, edi
var err error
params := githubv4.UpdatePullRequestInput{
PullRequestID: id,
Title: editable.TitleParam(),
Body: editable.BodyParam(),
Title: ghString(editable.TitleValue()),
Body: ghString(editable.BodyValue()),
}
params.AssigneeIDs, err = editable.AssigneesParam(client, repo)
assigneeIds, err := editable.AssigneeIds(client, repo)
if err != nil {
return err
}
params.LabelIDs, err = editable.LabelsParam()
params.AssigneeIDs = ghIds(assigneeIds)
labelIds, err := editable.LabelIds()
if err != nil {
return err
}
params.ProjectIDs, err = editable.ProjectsParam()
params.LabelIDs = ghIds(labelIds)
projectIds, err := editable.ProjectIds()
if err != nil {
return err
}
params.MilestoneID, err = editable.MilestoneParam()
params.ProjectIDs = ghIds(projectIds)
milestoneId, err := editable.MilestoneId()
if err != nil {
return err
}
params.MilestoneID = ghId(milestoneId)
err = api.UpdatePullRequest(client, repo, params)
if err != nil {
return err
@ -204,10 +211,10 @@ func updatePullRequest(client *api.Client, repo ghrepo.Interface, id string, edi
}
func updatePullRequestReviews(client *api.Client, repo ghrepo.Interface, id string, editable shared.Editable) error {
if !editable.ReviewersEdited {
if !editable.Reviewers.Edited {
return nil
}
userIds, teamIds, err := editable.ReviewersParams()
userIds, teamIds, err := editable.ReviewerIds()
if err != nil {
return err
}
@ -215,8 +222,8 @@ func updatePullRequestReviews(client *api.Client, repo ghrepo.Interface, id stri
reviewsRequestParams := githubv4.RequestReviewsInput{
PullRequestID: id,
Union: &union,
UserIDs: userIds,
TeamIDs: teamIds,
UserIDs: ghIds(userIds),
TeamIDs: ghIds(teamIds),
}
return api.UpdatePullRequestReviews(client, repo, reviewsRequestParams)
}
@ -257,3 +264,34 @@ type editorRetriever struct {
func (e editorRetriever) Retrieve() (string, error) {
return cmdutil.DetermineEditor(e.config)
}
func ghIds(s *[]string) *[]githubv4.ID {
if s == nil {
return nil
}
ids := make([]githubv4.ID, len(*s))
for i, v := range *s {
ids[i] = v
}
return &ids
}
func ghId(s *string) *githubv4.ID {
if s == nil {
return nil
}
if *s == "" {
r := githubv4.ID(nil)
return &r
}
r := githubv4.ID(*s)
return &r
}
func ghString(s *string) *githubv4.String {
if s == nil {
return nil
}
r := githubv4.String(*s)
return &r
}

View file

@ -29,7 +29,7 @@ func TestNewCmdEdit(t *testing.T) {
wantsErr: true,
},
{
name: "issue number argument",
name: "pull request number argument",
input: "23",
output: EditOptions{
SelectorArg: "23",
@ -43,8 +43,10 @@ func TestNewCmdEdit(t *testing.T) {
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Title: "test",
TitleEdited: true,
Title: shared.EditableString{
Value: "test",
Edited: true,
},
},
},
wantsErr: false,
@ -55,56 +57,122 @@ func TestNewCmdEdit(t *testing.T) {
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Body: "test",
BodyEdited: true,
Body: shared.EditableString{
Value: "test",
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "reviewer flag",
input: "23 --reviewer owner/team,monalisa",
name: "add-reviewer flag",
input: "23 --add-reviewer monalisa,owner/core",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Reviewers: []string{"owner/team", "monalisa"},
ReviewersEdited: true,
Reviewers: shared.EditableSlice{
Add: []string{"monalisa", "owner/core"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "assignee flag",
input: "23 --assignee monalisa,hubot",
name: "remove-reviewer flag",
input: "23 --remove-reviewer monalisa,owner/core",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Assignees: []string{"monalisa", "hubot"},
AssigneesEdited: true,
Reviewers: shared.EditableSlice{
Remove: []string{"monalisa", "owner/core"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "label flag",
input: "23 --label feature,TODO,bug",
name: "add-assignee flag",
input: "23 --add-assignee monalisa,hubot",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Labels: []string{"feature", "TODO", "bug"},
LabelsEdited: true,
Assignees: shared.EditableSlice{
Add: []string{"monalisa", "hubot"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "project flag",
input: "23 --project Cleanup,Roadmap",
name: "remove-assignee flag",
input: "23 --remove-assignee monalisa,hubot",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Projects: []string{"Cleanup", "Roadmap"},
ProjectsEdited: true,
Assignees: shared.EditableSlice{
Remove: []string{"monalisa", "hubot"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "add-label flag",
input: "23 --add-label feature,TODO,bug",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Labels: shared.EditableSlice{
Add: []string{"feature", "TODO", "bug"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "remove-label flag",
input: "23 --remove-label feature,TODO,bug",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Labels: shared.EditableSlice{
Remove: []string{"feature", "TODO", "bug"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "add-project flag",
input: "23 --add-project Cleanup,Roadmap",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Projects: shared.EditableSlice{
Add: []string{"Cleanup", "Roadmap"},
Edited: true,
},
},
},
wantsErr: false,
},
{
name: "remove-project flag",
input: "23 --remove-project Cleanup,Roadmap",
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Projects: shared.EditableSlice{
Remove: []string{"Cleanup", "Roadmap"},
Edited: true,
},
},
},
wantsErr: false,
@ -115,8 +183,10 @@ func TestNewCmdEdit(t *testing.T) {
output: EditOptions{
SelectorArg: "23",
Editable: shared.Editable{
Milestone: "GA",
MilestoneEdited: true,
Milestone: shared.EditableString{
Value: "GA",
Edited: true,
},
},
},
wantsErr: false,
@ -176,20 +246,38 @@ func Test_editRun(t *testing.T) {
SelectorArg: "123",
Interactive: false,
Editable: shared.Editable{
Title: "new title",
TitleEdited: true,
Body: "new body",
BodyEdited: true,
Reviewers: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot"},
ReviewersEdited: true,
Assignees: []string{"monalisa", "hubot"},
AssigneesEdited: true,
Labels: []string{"feature", "TODO", "bug"},
LabelsEdited: true,
Projects: []string{"Cleanup", "Roadmap"},
ProjectsEdited: true,
Milestone: "GA",
MilestoneEdited: true,
Title: shared.EditableString{
Value: "new title",
Edited: true,
},
Body: shared.EditableString{
Value: "new body",
Edited: true,
},
Reviewers: shared.EditableSlice{
Add: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot"},
Remove: []string{"dependabot"},
Edited: true,
},
Assignees: shared.EditableSlice{
Add: []string{"monalisa", "hubot"},
Remove: []string{"octocat"},
Edited: true,
},
Labels: shared.EditableSlice{
Add: []string{"feature", "TODO", "bug"},
Remove: []string{"docs"},
Edited: true,
},
Projects: shared.EditableSlice{
Add: []string{"Cleanup", "Roadmap"},
Remove: []string{"Features"},
Edited: true,
},
Milestone: shared.EditableString{
Value: "GA",
Edited: true,
},
},
Fetcher: testFetcher{},
},
@ -207,18 +295,33 @@ func Test_editRun(t *testing.T) {
SelectorArg: "123",
Interactive: false,
Editable: shared.Editable{
Title: "new title",
TitleEdited: true,
Body: "new body",
BodyEdited: true,
Assignees: []string{"monalisa", "hubot"},
AssigneesEdited: true,
Labels: []string{"feature", "TODO", "bug"},
LabelsEdited: true,
Projects: []string{"Cleanup", "Roadmap"},
ProjectsEdited: true,
Milestone: "GA",
MilestoneEdited: true,
Title: shared.EditableString{
Value: "new title",
Edited: true,
},
Body: shared.EditableString{
Value: "new body",
Edited: true,
},
Assignees: shared.EditableSlice{
Add: []string{"monalisa", "hubot"},
Remove: []string{"octocat"},
Edited: true,
},
Labels: shared.EditableSlice{
Value: []string{"feature", "TODO", "bug"},
Remove: []string{"docs"},
Edited: true,
},
Projects: shared.EditableSlice{
Value: []string{"Cleanup", "Roadmap"},
Remove: []string{"Features"},
Edited: true,
},
Milestone: shared.EditableString{
Value: "GA",
Edited: true,
},
},
Fetcher: testFetcher{},
},
@ -405,28 +508,28 @@ func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interf
}
func (s testSurveyor) FieldsToEdit(e *shared.Editable) error {
e.TitleEdited = true
e.BodyEdited = true
e.Title.Edited = true
e.Body.Edited = true
if !s.skipReviewers {
e.ReviewersEdited = true
e.Reviewers.Edited = true
}
e.AssigneesEdited = true
e.LabelsEdited = true
e.ProjectsEdited = true
e.MilestoneEdited = true
e.Assignees.Edited = true
e.Labels.Edited = true
e.Projects.Edited = true
e.Milestone.Edited = true
return nil
}
func (s testSurveyor) EditFields(e *shared.Editable, _ string) error {
e.Title = "new title"
e.Body = "new body"
e.Title.Value = "new title"
e.Body.Value = "new body"
if !s.skipReviewers {
e.Reviewers = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"}
e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"}
}
e.Assignees = []string{"monalisa", "hubot"}
e.Labels = []string{"feature", "TODO", "bug"}
e.Projects = []string{"Cleanup", "Roadmap"}
e.Milestone = "GA"
e.Assignees.Value = []string{"monalisa", "hubot"}
e.Labels.Value = []string{"feature", "TODO", "bug"}
e.Projects.Value = []string{"Cleanup", "Roadmap"}
e.Milestone.Value = "GA"
return nil
}

View file

@ -7,174 +7,199 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/set"
"github.com/cli/cli/pkg/surveyext"
"github.com/shurcooL/githubv4"
)
type Editable struct {
Title string
TitleDefault string
TitleEdited bool
Title EditableString
Body EditableString
Reviewers EditableSlice
Assignees EditableSlice
Labels EditableSlice
Projects EditableSlice
Milestone EditableString
Metadata api.RepoMetadataResult
}
Body string
BodyDefault string
BodyEdited bool
type EditableString struct {
Value string
Default string
Options []string
Edited bool
}
Reviewers []string
ReviewersDefault api.ReviewRequests
ReviewersOptions []string
ReviewersEdited bool
ReviewersAllowed bool
Assignees []string
AssigneesDefault api.Assignees
AssigneesOptions []string
AssigneesEdited bool
Labels []string
LabelsDefault api.Labels
LabelsOptions []string
LabelsEdited bool
Projects []string
ProjectsDefault api.ProjectCards
ProjectsOptions []string
ProjectsEdited bool
Milestone string
MilestoneDefault api.Milestone
MilestoneOptions []string
MilestoneEdited bool
Metadata api.RepoMetadataResult
type EditableSlice struct {
Value []string
Add []string
Remove []string
Default []string
Options []string
Edited bool
Allowed bool
}
func (e Editable) Dirty() bool {
return e.TitleEdited ||
e.BodyEdited ||
e.ReviewersEdited ||
e.AssigneesEdited ||
e.LabelsEdited ||
e.ProjectsEdited ||
e.MilestoneEdited
return e.Title.Edited ||
e.Body.Edited ||
e.Reviewers.Edited ||
e.Assignees.Edited ||
e.Labels.Edited ||
e.Projects.Edited ||
e.Milestone.Edited
}
func (e Editable) TitleParam() *githubv4.String {
if !e.TitleEdited {
func (e Editable) TitleValue() *string {
if !e.Title.Edited {
return nil
}
s := githubv4.String(e.Title)
return &s
return &e.Title.Value
}
func (e Editable) BodyParam() *githubv4.String {
if !e.BodyEdited {
func (e Editable) BodyValue() *string {
if !e.Body.Edited {
return nil
}
s := githubv4.String(e.Body)
return &s
return &e.Body.Value
}
func (e Editable) ReviewersParams() (*[]githubv4.ID, *[]githubv4.ID, error) {
if !e.ReviewersEdited {
func (e Editable) ReviewerIds() (*[]string, *[]string, error) {
if !e.Reviewers.Edited {
return nil, nil, nil
}
if len(e.Reviewers.Add) != 0 || len(e.Reviewers.Remove) != 0 {
s := set.NewStringSet()
s.AddValues(e.Reviewers.Default)
s.AddValues(e.Reviewers.Add)
s.RemoveValues(e.Reviewers.Remove)
e.Reviewers.Value = s.ToSlice()
}
var userReviewers []string
var teamReviewers []string
for _, r := range e.Reviewers {
for _, r := range e.Reviewers.Value {
if strings.ContainsRune(r, '/') {
teamReviewers = append(teamReviewers, r)
} else {
userReviewers = append(userReviewers, r)
}
}
userIds, err := toParams(userReviewers, e.Metadata.MembersToIDs)
userIds, err := e.Metadata.MembersToIDs(userReviewers)
if err != nil {
return nil, nil, err
}
teamIds, err := toParams(teamReviewers, e.Metadata.TeamsToIDs)
teamIds, err := e.Metadata.TeamsToIDs(teamReviewers)
if err != nil {
return nil, nil, err
}
return userIds, teamIds, nil
return &userIds, &teamIds, nil
}
func (e Editable) AssigneesParam(client *api.Client, repo ghrepo.Interface) (*[]githubv4.ID, error) {
if !e.AssigneesEdited {
func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]string, error) {
if !e.Assignees.Edited {
return nil, nil
}
meReplacer := NewMeReplacer(client, repo.RepoHost())
assignees, err := meReplacer.ReplaceSlice(e.Assignees)
if err != nil {
return nil, err
if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 {
meReplacer := NewMeReplacer(client, repo.RepoHost())
s := set.NewStringSet()
s.AddValues(e.Assignees.Default)
add, err := meReplacer.ReplaceSlice(e.Assignees.Add)
if err != nil {
return nil, err
}
s.AddValues(add)
remove, err := meReplacer.ReplaceSlice(e.Assignees.Remove)
if err != nil {
return nil, err
}
s.RemoveValues(remove)
e.Assignees.Value = s.ToSlice()
}
return toParams(assignees, e.Metadata.MembersToIDs)
a, err := e.Metadata.MembersToIDs(e.Assignees.Value)
return &a, err
}
func (e Editable) LabelsParam() (*[]githubv4.ID, error) {
if !e.LabelsEdited {
func (e Editable) LabelIds() (*[]string, error) {
if !e.Labels.Edited {
return nil, nil
}
return toParams(e.Labels, e.Metadata.LabelsToIDs)
if len(e.Labels.Add) != 0 || len(e.Labels.Remove) != 0 {
s := set.NewStringSet()
s.AddValues(e.Labels.Default)
s.AddValues(e.Labels.Add)
s.RemoveValues(e.Labels.Remove)
e.Labels.Value = s.ToSlice()
}
l, err := e.Metadata.LabelsToIDs(e.Labels.Value)
return &l, err
}
func (e Editable) ProjectsParam() (*[]githubv4.ID, error) {
if !e.ProjectsEdited {
func (e Editable) ProjectIds() (*[]string, error) {
if !e.Projects.Edited {
return nil, nil
}
return toParams(e.Projects, e.Metadata.ProjectsToIDs)
if len(e.Projects.Add) != 0 || len(e.Projects.Remove) != 0 {
s := set.NewStringSet()
s.AddValues(e.Projects.Default)
s.AddValues(e.Projects.Add)
s.RemoveValues(e.Projects.Remove)
e.Projects.Value = s.ToSlice()
}
p, err := e.Metadata.ProjectsToIDs(e.Projects.Value)
return &p, err
}
func (e Editable) MilestoneParam() (*githubv4.ID, error) {
if !e.MilestoneEdited {
func (e Editable) MilestoneId() (*string, error) {
if !e.Milestone.Edited {
return nil, nil
}
if e.Milestone == noMilestone || e.Milestone == "" {
return githubv4.NewID(nil), nil
if e.Milestone.Value == noMilestone || e.Milestone.Value == "" {
s := ""
return &s, nil
}
return toParam(e.Milestone, e.Metadata.MilestoneToID)
m, err := e.Metadata.MilestoneToID(e.Milestone.Value)
return &m, err
}
func EditFieldsSurvey(editable *Editable, editorCommand string) error {
var err error
if editable.TitleEdited {
editable.Title, err = titleSurvey(editable.TitleDefault)
if editable.Title.Edited {
editable.Title.Value, err = titleSurvey(editable.Title.Default)
if err != nil {
return err
}
}
if editable.BodyEdited {
editable.Body, err = bodySurvey(editable.BodyDefault, editorCommand)
if editable.Body.Edited {
editable.Body.Value, err = bodySurvey(editable.Body.Default, editorCommand)
if err != nil {
return err
}
}
if editable.ReviewersEdited {
editable.Reviewers, err = reviewersSurvey(editable.ReviewersDefault, editable.ReviewersOptions)
if editable.Reviewers.Edited {
editable.Reviewers.Value, err = multiSelectSurvey("Reviewers", editable.Reviewers.Default, editable.Reviewers.Options)
if err != nil {
return err
}
}
if editable.AssigneesEdited {
editable.Assignees, err = assigneesSurvey(editable.AssigneesDefault, editable.AssigneesOptions)
if editable.Assignees.Edited {
editable.Assignees.Value, err = multiSelectSurvey("Assignees", editable.Assignees.Default, editable.Assignees.Options)
if err != nil {
return err
}
}
if editable.LabelsEdited {
editable.Labels, err = labelsSurvey(editable.LabelsDefault, editable.LabelsOptions)
if editable.Labels.Edited {
editable.Labels.Value, err = multiSelectSurvey("Labels", editable.Labels.Default, editable.Labels.Options)
if err != nil {
return err
}
}
if editable.ProjectsEdited {
editable.Projects, err = projectsSurvey(editable.ProjectsDefault, editable.ProjectsOptions)
if editable.Projects.Edited {
editable.Projects.Value, err = multiSelectSurvey("Projects", editable.Projects.Default, editable.Projects.Options)
if err != nil {
return err
}
}
if editable.MilestoneEdited {
editable.Milestone, err = milestoneSurvey(editable.MilestoneDefault, editable.MilestoneOptions)
if editable.Milestone.Edited {
editable.Milestone.Value, err = milestoneSurvey(editable.Milestone.Default, editable.Milestone.Options)
if err != nil {
return err
}
@ -200,41 +225,36 @@ func FieldsToEditSurvey(editable *Editable) error {
return false
}
results := []string{}
opts := []string{"Title", "Body"}
if editable.ReviewersAllowed {
if editable.Reviewers.Allowed {
opts = append(opts, "Reviewers")
}
opts = append(opts, "Assignees", "Labels", "Projects", "Milestone")
q := &survey.MultiSelect{
Message: "What would you like to edit?",
Options: opts,
}
err := survey.AskOne(q, &results)
results, err := multiSelectSurvey("What would you like to edit?", []string{}, opts)
if err != nil {
return err
}
if contains(results, "Title") {
editable.TitleEdited = true
editable.Title.Edited = true
}
if contains(results, "Body") {
editable.BodyEdited = true
editable.Body.Edited = true
}
if contains(results, "Reviewers") {
editable.ReviewersEdited = true
editable.Reviewers.Edited = true
}
if contains(results, "Assignees") {
editable.AssigneesEdited = true
editable.Assignees.Edited = true
}
if contains(results, "Labels") {
editable.LabelsEdited = true
editable.Labels.Edited = true
}
if contains(results, "Projects") {
editable.ProjectsEdited = true
editable.Projects.Edited = true
}
if contains(results, "Milestone") {
editable.MilestoneEdited = true
editable.Milestone.Edited = true
}
return nil
@ -242,11 +262,11 @@ func FieldsToEditSurvey(editable *Editable) error {
func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error {
input := api.RepoMetadataInput{
Reviewers: editable.ReviewersEdited,
Assignees: editable.AssigneesEdited,
Labels: editable.LabelsEdited,
Projects: editable.ProjectsEdited,
Milestones: editable.MilestoneEdited,
Reviewers: editable.Reviewers.Edited,
Assignees: editable.Assignees.Edited,
Labels: editable.Labels.Edited,
Projects: editable.Projects.Edited,
Milestones: editable.Milestone.Edited,
}
metadata, err := api.RepoMetadata(client, repo, input)
if err != nil {
@ -275,11 +295,11 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable)
}
editable.Metadata = *metadata
editable.ReviewersOptions = append(users, teams...)
editable.AssigneesOptions = users
editable.LabelsOptions = labels
editable.ProjectsOptions = projects
editable.MilestoneOptions = milestones
editable.Reviewers.Options = append(users, teams...)
editable.Assignees.Options = users
editable.Labels.Options = labels
editable.Projects.Options = projects
editable.Milestone.Options = milestones
return nil
}
@ -297,9 +317,9 @@ func titleSurvey(title string) (string, error) {
func bodySurvey(body, editorCommand string) (string, error) {
var result string
q := &surveyext.GhEditor{
BlankAllowed: true,
EditorCommand: editorCommand,
Editor: &survey.Editor{Message: "Body",
Editor: &survey.Editor{
Message: "Body",
FileName: "*.md",
Default: body,
HideDefault: true,
@ -310,79 +330,21 @@ func bodySurvey(body, editorCommand string) (string, error) {
return result, err
}
func reviewersSurvey(reviewers api.ReviewRequests, opts []string) ([]string, error) {
if len(opts) == 0 {
func multiSelectSurvey(message string, defaults, options []string) ([]string, error) {
if len(options) == 0 {
return nil, nil
}
logins := []string{}
for _, a := range reviewers.Nodes {
logins = append(logins, a.RequestedReviewer.Login)
}
var results []string
q := &survey.MultiSelect{
Message: "Reviewers",
Options: opts,
Default: logins,
Message: message,
Options: options,
Default: defaults,
}
err := survey.AskOne(q, &results)
return results, err
}
func assigneesSurvey(assignees api.Assignees, opts []string) ([]string, error) {
if len(opts) == 0 {
return nil, nil
}
logins := []string{}
for _, a := range assignees.Nodes {
logins = append(logins, a.Login)
}
var results []string
q := &survey.MultiSelect{
Message: "Assignees",
Options: opts,
Default: logins,
}
err := survey.AskOne(q, &results)
return results, err
}
func labelsSurvey(labels api.Labels, opts []string) ([]string, error) {
if len(opts) == 0 {
return nil, nil
}
names := []string{}
for _, l := range labels.Nodes {
names = append(names, l.Name)
}
var results []string
q := &survey.MultiSelect{
Message: "Labels",
Options: opts,
Default: names,
}
err := survey.AskOne(q, &results)
return results, err
}
func projectsSurvey(projectCards api.ProjectCards, opts []string) ([]string, error) {
if len(opts) == 0 {
return nil, nil
}
names := []string{}
for _, c := range projectCards.Nodes {
names = append(names, c.Project.Name)
}
var results []string
q := &survey.MultiSelect{
Message: "Projects",
Options: opts,
Default: names,
}
err := survey.AskOne(q, &results)
return results, err
}
func milestoneSurvey(milestone api.Milestone, opts []string) (string, error) {
func milestoneSurvey(title string, opts []string) (string, error) {
if len(opts) == 0 {
return "", nil
}
@ -390,7 +352,7 @@ func milestoneSurvey(milestone api.Milestone, opts []string) (string, error) {
q := &survey.Select{
Message: "Milestone",
Options: opts,
Default: milestone.Title,
Default: title,
}
err := survey.AskOne(q, &result)
return result, err
@ -405,24 +367,3 @@ func confirmSurvey() (bool, error) {
err := survey.AskOne(q, &result)
return result, err
}
func toParams(s []string, mapper func([]string) ([]string, error)) (*[]githubv4.ID, error) {
ids, err := mapper(s)
if err != nil {
return nil, err
}
gIds := make([]githubv4.ID, len(ids))
for i, v := range ids {
gIds[i] = v
}
return &gIds, nil
}
func toParam(s string, mapper func(string) (string, error)) (*githubv4.ID, error) {
id, err := mapper(s)
if err != nil {
return nil, err
}
gId := githubv4.ID(id)
return &gId, nil
}

46
pkg/set/string_set.go Normal file
View file

@ -0,0 +1,46 @@
package set
var exists = struct{}{}
type stringSet struct {
m map[string]struct{}
}
func NewStringSet() *stringSet {
s := &stringSet{}
s.m = make(map[string]struct{})
return s
}
func (s *stringSet) Add(value string) {
s.m[value] = exists
}
func (s *stringSet) AddValues(values []string) {
for _, v := range values {
s.Add(v)
}
}
func (s *stringSet) Remove(value string) {
delete(s.m, value)
}
func (s *stringSet) RemoveValues(values []string) {
for _, v := range values {
s.Remove(v)
}
}
func (s *stringSet) Contains(value string) bool {
_, c := s.m[value]
return c
}
func (s *stringSet) ToSlice() []string {
r := make([]string, 0, len(s.m))
for k := range s.m {
r = append(r, k)
}
return r
}