diff --git a/api/queries_issue.go b/api/queries_issue.go index 11c169e0e..bd3f15555 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -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 } diff --git a/api/queries_pr.go b/api/queries_pr.go index e91216479..4c35a153a 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -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!) { diff --git a/api/queries_repo.go b/api/queries_repo.go index 852664b7c..90d3949a5 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -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) { diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index c209c8fc7..e83784359 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -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 +} diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index b132db26f..d21997516 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -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, diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index 0bd5ce826..6c1592b4c 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -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 +} diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go index 1351ff29a..35d8278dc 100644 --- a/pkg/cmd/pr/edit/edit_test.go +++ b/pkg/cmd/pr/edit/edit_test.go @@ -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 } diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index 885d1459d..16faca4de 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -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 -} diff --git a/pkg/set/string_set.go b/pkg/set/string_set.go new file mode 100644 index 000000000..c7e7726a4 --- /dev/null +++ b/pkg/set/string_set.go @@ -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 +}