Edit issue command
This commit is contained in:
parent
481ec92ebf
commit
b366802aa1
6 changed files with 964 additions and 43 deletions
|
|
@ -24,44 +24,52 @@ type IssuesAndTotalCount struct {
|
|||
}
|
||||
|
||||
type Issue struct {
|
||||
ID string
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
State string
|
||||
Closed bool
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Comments Comments
|
||||
Author Author
|
||||
Assignees struct {
|
||||
Nodes []struct {
|
||||
Login string
|
||||
}
|
||||
TotalCount int
|
||||
ID string
|
||||
Number int
|
||||
Title string
|
||||
URL string
|
||||
State string
|
||||
Closed bool
|
||||
Body string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Comments Comments
|
||||
Author Author
|
||||
Assignees Assignees
|
||||
Labels Labels
|
||||
ProjectCards ProjectCards
|
||||
Milestone Milestone
|
||||
ReactionGroups ReactionGroups
|
||||
}
|
||||
|
||||
type Assignees struct {
|
||||
Nodes []struct {
|
||||
Login string
|
||||
}
|
||||
Labels struct {
|
||||
Nodes []struct {
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
type Labels struct {
|
||||
Nodes []struct {
|
||||
Name string
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
type ProjectCards struct {
|
||||
Nodes []struct {
|
||||
Project struct {
|
||||
Name string
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
ProjectCards struct {
|
||||
Nodes []struct {
|
||||
Project struct {
|
||||
Name string
|
||||
}
|
||||
Column struct {
|
||||
Name string
|
||||
}
|
||||
Column struct {
|
||||
Name string
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
Milestone struct {
|
||||
Title string
|
||||
}
|
||||
ReactionGroups ReactionGroups
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
type Milestone struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
type IssuesDisabledError struct {
|
||||
|
|
@ -488,6 +496,20 @@ func IssueDelete(client *Client, repo ghrepo.Interface, issue Issue) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func IssueUpdate(client *Client, repo ghrepo.Interface, params githubv4.UpdateIssueInput) error {
|
||||
var mutation struct {
|
||||
UpdateIssue struct {
|
||||
Issue struct {
|
||||
ID string
|
||||
}
|
||||
} `graphql:"updateIssue(input: $input)"`
|
||||
}
|
||||
variables := map[string]interface{}{"input": params}
|
||||
gql := graphQLClient(client.http, repo.RepoHost())
|
||||
err := gql.MutateNamed(context.Background(), "IssueUpdate", &mutation, variables)
|
||||
return err
|
||||
}
|
||||
|
||||
// milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID
|
||||
// This conversion is necessary since the GraphQL API requires the use of the milestone's database ID
|
||||
// for querying the related issues.
|
||||
|
|
|
|||
|
|
@ -80,16 +80,6 @@ type PullRequest struct {
|
|||
}
|
||||
}
|
||||
}
|
||||
ReviewRequests struct {
|
||||
Nodes []struct {
|
||||
RequestedReviewer struct {
|
||||
TypeName string `json:"__typename"`
|
||||
Login string
|
||||
Name string
|
||||
}
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
Assignees struct {
|
||||
Nodes []struct {
|
||||
Login string
|
||||
|
|
@ -119,6 +109,18 @@ type PullRequest struct {
|
|||
Comments Comments
|
||||
ReactionGroups ReactionGroups
|
||||
Reviews PullRequestReviews
|
||||
ReviewRequests ReviewRequests
|
||||
}
|
||||
|
||||
type ReviewRequests struct {
|
||||
Nodes []struct {
|
||||
RequestedReviewer struct {
|
||||
TypeName string `json:"__typename"`
|
||||
Login string
|
||||
Name string
|
||||
}
|
||||
}
|
||||
TotalCount int
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
|
|
|
|||
236
pkg/cmd/issue/edit/edit.go
Normal file
236
pkg/cmd/issue/edit/edit.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
shared "github.com/cli/cli/pkg/cmd/issue/shared"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type EditOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
|
||||
OpenInBrowser func(string) error
|
||||
DetermineEditor func() (string, error)
|
||||
FieldsToEditSurvey func(*prShared.EditableOptions) error
|
||||
EditableSurvey func(string, *prShared.EditableOptions) error
|
||||
FetchOptions func(*api.Client, ghrepo.Interface, *prShared.EditableOptions) error
|
||||
|
||||
SelectorArg string
|
||||
Interactive bool
|
||||
WebMode bool
|
||||
|
||||
prShared.EditableOptions
|
||||
}
|
||||
|
||||
func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Command {
|
||||
opts := &EditOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
OpenInBrowser: utils.OpenInBrowser,
|
||||
DetermineEditor: func() (string, error) { return cmdutil.DetermineEditor(f.Config) },
|
||||
FieldsToEditSurvey: prShared.FieldsToEditSurvey,
|
||||
EditableSurvey: prShared.EditableSurvey,
|
||||
FetchOptions: prShared.FetchOptions,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "edit {<number> | <url>}",
|
||||
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"
|
||||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
opts.SelectorArg = args[0]
|
||||
|
||||
flags := cmd.Flags()
|
||||
if flags.Changed("title") {
|
||||
opts.EditableOptions.TitleEdited = true
|
||||
}
|
||||
if flags.Changed("body") {
|
||||
opts.EditableOptions.BodyEdited = true
|
||||
}
|
||||
if flags.Changed("assignee") {
|
||||
opts.EditableOptions.AssigneesEdited = true
|
||||
}
|
||||
if flags.Changed("label") {
|
||||
opts.EditableOptions.LabelsEdited = true
|
||||
}
|
||||
if flags.Changed("project") {
|
||||
opts.EditableOptions.ProjectsEdited = true
|
||||
}
|
||||
if flags.Changed("milestone") {
|
||||
opts.EditableOptions.MilestoneEdited = true
|
||||
}
|
||||
|
||||
if !opts.EditableOptions.Dirty() {
|
||||
opts.Interactive = true
|
||||
}
|
||||
|
||||
if opts.Interactive && !opts.IO.CanPrompt() {
|
||||
return &cmdutil.FlagError{Err: errors.New("--tile, --body, --assignee, --label, --project, --milestone, or --web required when not running interactively")}
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return editRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to create an issue")
|
||||
cmd.Flags().StringVarP(&opts.EditableOptions.Title, "title", "t", "", "Supply a title. Will prompt for one otherwise.")
|
||||
cmd.Flags().StringVarP(&opts.EditableOptions.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
|
||||
cmd.Flags().StringSliceVarP(&opts.EditableOptions.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.")
|
||||
cmd.Flags().StringSliceVarP(&opts.EditableOptions.Labels, "label", "l", nil, "Add labels by `name`")
|
||||
cmd.Flags().StringSliceVarP(&opts.EditableOptions.Projects, "project", "p", nil, "Add the issue to projects by `name`")
|
||||
cmd.Flags().StringVarP(&opts.EditableOptions.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func editRun(opts *EditOptions) error {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
issue, repo, err := shared.IssueFromArg(apiClient, opts.BaseRepo, opts.SelectorArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.WebMode {
|
||||
openURL := issue.URL
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return opts.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
editOptions := opts.EditableOptions
|
||||
editOptions.TitleDefault = issue.Title
|
||||
editOptions.BodyDefault = issue.Body
|
||||
editOptions.AssigneesDefault = issue.Assignees
|
||||
editOptions.LabelsDefault = issue.Labels
|
||||
editOptions.ProjectsDefault = issue.ProjectCards
|
||||
editOptions.MilestoneDefault = issue.Milestone
|
||||
|
||||
if opts.Interactive {
|
||||
err = opts.FieldsToEditSurvey(&editOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
err = opts.FetchOptions(apiClient, repo, &editOptions)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Interactive {
|
||||
editorCommand, err := opts.DetermineEditor()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = opts.EditableSurvey(editorCommand, &editOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
err = updateIssue(apiClient, repo, issue.ID, editOptions)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(opts.IO.Out, issue.URL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateIssue(client *api.Client, repo ghrepo.Interface, id string, options prShared.EditableOptions) error {
|
||||
params := githubv4.UpdateIssueInput{ID: id}
|
||||
if options.TitleEdited {
|
||||
title := githubv4.String(options.Title)
|
||||
params.Title = &title
|
||||
}
|
||||
if options.BodyEdited {
|
||||
body := githubv4.String(options.Body)
|
||||
params.Body = &body
|
||||
}
|
||||
if options.AssigneesEdited {
|
||||
meReplacer := prShared.NewMeReplacer(client, repo.RepoHost())
|
||||
assignees, err := meReplacer.ReplaceSlice(options.Assignees)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids, err := options.Metadata.MembersToIDs(assignees)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assigneeIDs := make([]githubv4.ID, len(ids))
|
||||
for i, v := range ids {
|
||||
assigneeIDs[i] = v
|
||||
}
|
||||
params.AssigneeIDs = &assigneeIDs
|
||||
}
|
||||
if options.LabelsEdited {
|
||||
ids, err := options.Metadata.LabelsToIDs(options.Labels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
labelIDs := make([]githubv4.ID, len(ids))
|
||||
for i, v := range ids {
|
||||
labelIDs[i] = v
|
||||
}
|
||||
params.LabelIDs = &labelIDs
|
||||
}
|
||||
if options.ProjectsEdited {
|
||||
ids, err := options.Metadata.ProjectsToIDs(options.Projects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectIDs := make([]githubv4.ID, len(ids))
|
||||
for i, v := range ids {
|
||||
projectIDs[i] = v
|
||||
}
|
||||
params.ProjectIDs = &projectIDs
|
||||
}
|
||||
if options.MilestoneEdited {
|
||||
id, err := options.Metadata.MilestoneToID(options.Milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
milestoneID := githubv4.ID(id)
|
||||
params.MilestoneID = &milestoneID
|
||||
}
|
||||
return api.IssueUpdate(client, repo, params)
|
||||
}
|
||||
349
pkg/cmd/issue/edit/edit_test.go
Normal file
349
pkg/cmd/issue/edit/edit_test.go
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdEdit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output EditOptions
|
||||
wantsErr bool
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
input: "",
|
||||
output: EditOptions{},
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "issue number argument",
|
||||
input: "23",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
Interactive: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "web flag",
|
||||
input: "23 --web",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
Interactive: true,
|
||||
WebMode: true,
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "title flag",
|
||||
input: "23 --title test",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
EditableOptions: prShared.EditableOptions{
|
||||
Title: "test",
|
||||
TitleEdited: true,
|
||||
},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "body flag",
|
||||
input: "23 --body test",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
EditableOptions: prShared.EditableOptions{
|
||||
Body: "test",
|
||||
BodyEdited: true,
|
||||
},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "assignee flag",
|
||||
input: "23 --assignee monalisa,hubot",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
EditableOptions: prShared.EditableOptions{
|
||||
Assignees: []string{"monalisa", "hubot"},
|
||||
AssigneesEdited: true,
|
||||
},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "label flag",
|
||||
input: "23 --label feature,TODO,bug",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
EditableOptions: prShared.EditableOptions{
|
||||
Labels: []string{"feature", "TODO", "bug"},
|
||||
LabelsEdited: true,
|
||||
},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "project flag",
|
||||
input: "23 --project Cleanup,Roadmap",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
EditableOptions: prShared.EditableOptions{
|
||||
Projects: []string{"Cleanup", "Roadmap"},
|
||||
ProjectsEdited: true,
|
||||
},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "milestone flag",
|
||||
input: "23 --milestone GA",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
EditableOptions: prShared.EditableOptions{
|
||||
Milestone: "GA",
|
||||
MilestoneEdited: true,
|
||||
},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
io, _, _, _ := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStderrTTY(true)
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *EditOptions
|
||||
cmd := NewCmdEdit(f, func(opts *EditOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
cmd.Flags().BoolP("help", "x", false, "")
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
|
||||
assert.Equal(t, tt.output.WebMode, gotOpts.WebMode)
|
||||
assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
|
||||
assert.Equal(t, tt.output.EditableOptions, gotOpts.EditableOptions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_editRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *EditOptions
|
||||
httpStubs func(*testing.T, *httpmock.Registry)
|
||||
stdout string
|
||||
stderr string
|
||||
}{
|
||||
{
|
||||
name: "web mode",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
WebMode: true,
|
||||
OpenInBrowser: func(string) error { return nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIssueGet(t, reg)
|
||||
},
|
||||
stderr: "Opening github.com/OWNER/REPO/issue/123 in your browser.\n",
|
||||
},
|
||||
{
|
||||
name: "non-interactive",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
Interactive: false,
|
||||
EditableOptions: prShared.EditableOptions{
|
||||
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,
|
||||
},
|
||||
FetchOptions: prShared.FetchOptions,
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIssueGet(t, reg)
|
||||
mockRepoMetadata(t, reg)
|
||||
mockIssueUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issue/123\n",
|
||||
},
|
||||
{
|
||||
name: "interactive",
|
||||
input: &EditOptions{
|
||||
SelectorArg: "123",
|
||||
Interactive: true,
|
||||
FieldsToEditSurvey: func(eo *prShared.EditableOptions) error {
|
||||
eo.TitleEdited = true
|
||||
eo.BodyEdited = true
|
||||
eo.AssigneesEdited = true
|
||||
eo.LabelsEdited = true
|
||||
eo.ProjectsEdited = true
|
||||
eo.MilestoneEdited = true
|
||||
return nil
|
||||
},
|
||||
EditableSurvey: func(_ string, eo *prShared.EditableOptions) 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"
|
||||
return nil
|
||||
},
|
||||
FetchOptions: prShared.FetchOptions,
|
||||
DetermineEditor: func() (string, error) { return "vim", nil },
|
||||
},
|
||||
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
||||
mockIssueGet(t, reg)
|
||||
mockRepoMetadata(t, reg)
|
||||
mockIssueUpdate(t, reg)
|
||||
},
|
||||
stdout: "https://github.com/OWNER/REPO/issue/123\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
io, _, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(true)
|
||||
io.SetStdinTTY(true)
|
||||
io.SetStderrTTY(true)
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
tt.httpStubs(t, reg)
|
||||
|
||||
httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
|
||||
baseRepo := func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
|
||||
tt.input.IO = io
|
||||
tt.input.HttpClient = httpClient
|
||||
tt.input.BaseRepo = baseRepo
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := editRun(tt.input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.stdout, stdout.String())
|
||||
assert.Equal(t, tt.stderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockIssueGet(_ *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query IssueByNumber\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
|
||||
"number": 123,
|
||||
"url": "https://github.com/OWNER/REPO/issue/123"
|
||||
} } } }`),
|
||||
)
|
||||
}
|
||||
|
||||
func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryAssignableUsers\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "assignableUsers": {
|
||||
"nodes": [
|
||||
{ "login": "hubot", "id": "HUBOTID" },
|
||||
{ "login": "MonaLisa", "id": "MONAID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryLabelList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "labels": {
|
||||
"nodes": [
|
||||
{ "name": "feature", "id": "FEATUREID" },
|
||||
{ "name": "TODO", "id": "TODOID" },
|
||||
{ "name": "bug", "id": "BUGID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryMilestoneList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "milestones": {
|
||||
"nodes": [
|
||||
{ "title": "GA", "id": "GAID" },
|
||||
{ "title": "Big One.oh", "id": "BIGONEID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query RepositoryProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Cleanup", "id": "CLEANUPID" },
|
||||
{ "name": "Roadmap", "id": "ROADMAPID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query OrganizationProjectList\b`),
|
||||
httpmock.StringResponse(`
|
||||
{ "data": { "organization": { "projects": {
|
||||
"nodes": [
|
||||
{ "name": "Triage", "id": "TRIAGEID" }
|
||||
],
|
||||
"pageInfo": { "hasNextPage": false }
|
||||
} } } }
|
||||
`))
|
||||
}
|
||||
|
||||
func mockIssueUpdate(t *testing.T, reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation IssueUpdate\b`),
|
||||
httpmock.GraphQLMutation(`
|
||||
{ "data": { "updateIssue": { "issue": {
|
||||
"id": "123"
|
||||
} } } }`,
|
||||
func(inputs map[string]interface{}) {}),
|
||||
)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
cmdComment "github.com/cli/cli/pkg/cmd/issue/comment"
|
||||
cmdCreate "github.com/cli/cli/pkg/cmd/issue/create"
|
||||
cmdDelete "github.com/cli/cli/pkg/cmd/issue/delete"
|
||||
cmdEdit "github.com/cli/cli/pkg/cmd/issue/edit"
|
||||
cmdList "github.com/cli/cli/pkg/cmd/issue/list"
|
||||
cmdReopen "github.com/cli/cli/pkg/cmd/issue/reopen"
|
||||
cmdStatus "github.com/cli/cli/pkg/cmd/issue/status"
|
||||
|
|
@ -44,6 +45,7 @@ func NewCmdIssue(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
cmd.AddCommand(cmdComment.NewCmdComment(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
cmd.AddCommand(cmdEdit.NewCmdEdit(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
310
pkg/cmd/pr/shared/editable.go
Normal file
310
pkg/cmd/pr/shared/editable.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
)
|
||||
|
||||
type EditableOptions struct {
|
||||
Title string
|
||||
TitleDefault string
|
||||
TitleEdited bool
|
||||
|
||||
Body string
|
||||
BodyDefault string
|
||||
BodyEdited 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
|
||||
}
|
||||
|
||||
func (e EditableOptions) Dirty() bool {
|
||||
return e.TitleEdited ||
|
||||
e.BodyEdited ||
|
||||
e.ReviewersEdited ||
|
||||
e.AssigneesEdited ||
|
||||
e.LabelsEdited ||
|
||||
e.ProjectsEdited ||
|
||||
e.MilestoneEdited
|
||||
}
|
||||
|
||||
func EditableSurvey(editorCommand string, options *EditableOptions) error {
|
||||
if options.TitleEdited {
|
||||
title, err := titleSurvey(options.TitleDefault)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Title = title
|
||||
}
|
||||
if options.BodyEdited {
|
||||
body, err := bodySurvey(options.BodyDefault, editorCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Body = body
|
||||
}
|
||||
if options.AssigneesEdited {
|
||||
assignees, err := assigneesSurvey(options.AssigneesDefault, options.AssigneesOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Assignees = assignees
|
||||
}
|
||||
if options.LabelsEdited {
|
||||
labels, err := labelsSurvey(options.LabelsDefault, options.LabelsOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Labels = labels
|
||||
}
|
||||
if options.ProjectsEdited {
|
||||
projects, err := projectsSurvey(options.ProjectsDefault, options.ProjectsOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Projects = projects
|
||||
}
|
||||
if options.MilestoneEdited {
|
||||
milestone, err := milestoneSurvey(options.MilestoneDefault, options.MilestoneOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.Milestone = milestone
|
||||
}
|
||||
confirm, err := confirmSurvey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirm {
|
||||
return fmt.Errorf("Discarding...")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FieldsToEditSurvey(options *EditableOptions) error {
|
||||
contains := func(s []string, str string) bool {
|
||||
for _, v := range s {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
results := []string{}
|
||||
opts := []string{"Title", "Body"}
|
||||
if options.ReviewersAllowed {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if contains(results, "Title") {
|
||||
options.TitleEdited = true
|
||||
}
|
||||
if contains(results, "Body") {
|
||||
options.BodyEdited = true
|
||||
}
|
||||
if contains(results, "Reviewers") {
|
||||
options.ReviewersEdited = true
|
||||
}
|
||||
if contains(results, "Assignees") {
|
||||
options.AssigneesEdited = true
|
||||
}
|
||||
if contains(results, "Labels") {
|
||||
options.LabelsEdited = true
|
||||
}
|
||||
if contains(results, "Projects") {
|
||||
options.ProjectsEdited = true
|
||||
}
|
||||
if contains(results, "Milestone") {
|
||||
options.MilestoneEdited = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FetchOptions(client *api.Client, repo ghrepo.Interface, options *EditableOptions) error {
|
||||
input := api.RepoMetadataInput{
|
||||
Reviewers: options.ReviewersEdited,
|
||||
Assignees: options.AssigneesEdited,
|
||||
Labels: options.LabelsEdited,
|
||||
Projects: options.ProjectsEdited,
|
||||
Milestones: options.MilestoneEdited,
|
||||
}
|
||||
metadata, err := api.RepoMetadata(client, repo, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var users []string
|
||||
for _, u := range metadata.AssignableUsers {
|
||||
users = append(users, u.Login)
|
||||
}
|
||||
var teams []string
|
||||
for _, t := range metadata.Teams {
|
||||
teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug))
|
||||
}
|
||||
var labels []string
|
||||
for _, l := range metadata.Labels {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
var projects []string
|
||||
for _, l := range metadata.Projects {
|
||||
projects = append(projects, l.Name)
|
||||
}
|
||||
milestones := []string{noMilestone}
|
||||
for _, m := range metadata.Milestones {
|
||||
milestones = append(milestones, m.Title)
|
||||
}
|
||||
|
||||
options.Metadata = *metadata
|
||||
options.ReviewersOptions = append(users, teams...)
|
||||
options.AssigneesOptions = users
|
||||
options.LabelsOptions = labels
|
||||
options.ProjectsOptions = projects
|
||||
options.MilestoneOptions = milestones
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func titleSurvey(title string) (string, error) {
|
||||
var result string
|
||||
q := &survey.Input{
|
||||
Message: "Title",
|
||||
Default: title,
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func bodySurvey(body, editorCommand string) (string, error) {
|
||||
var result string
|
||||
q := &surveyext.GhEditor{
|
||||
BlankAllowed: true,
|
||||
EditorCommand: editorCommand,
|
||||
Editor: &survey.Editor{Message: "Body",
|
||||
FileName: "*.md",
|
||||
Default: body,
|
||||
HideDefault: true,
|
||||
AppendDefault: true,
|
||||
},
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func assigneesSurvey(assignees api.Assignees, assigneesOpts []string) ([]string, error) {
|
||||
if len(assigneesOpts) == 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: assigneesOpts,
|
||||
Default: logins,
|
||||
}
|
||||
err := survey.AskOne(q, &results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func labelsSurvey(labels api.Labels, labelOpts []string) ([]string, error) {
|
||||
if len(labelOpts) == 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: labelOpts,
|
||||
Default: names,
|
||||
}
|
||||
err := survey.AskOne(q, &results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func projectsSurvey(projectCards api.ProjectCards, projectsOpts []string) ([]string, error) {
|
||||
if len(projectsOpts) == 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: projectsOpts,
|
||||
Default: names,
|
||||
}
|
||||
err := survey.AskOne(q, &results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
func milestoneSurvey(milestone api.Milestone, milestoneOpts []string) (string, error) {
|
||||
if len(milestoneOpts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
var result string
|
||||
q := &survey.Select{
|
||||
Message: "Milestone",
|
||||
Options: milestoneOpts,
|
||||
Default: milestone.Title,
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func confirmSurvey() (bool, error) {
|
||||
var result bool
|
||||
q := &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
}
|
||||
err := survey.AskOne(q, &result)
|
||||
return result, err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue