Add label edit command (#5519)

This commit is contained in:
Heath Stewart 2022-04-26 02:43:07 -07:00 committed by GitHub
parent db85b1d3cb
commit 6edb4ecdbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 305 additions and 11 deletions

View file

@ -60,12 +60,13 @@ func newCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co
Long: heredoc.Doc(`
Create a new label on GitHub.
Must specify name for the label, the description and color are optional.
Must specify name for the label. The description and color are optional.
If a color isn't provided, a random one will be chosen.
Color needs to be 6 character hex value.
The label color needs to be 6 character hex value.
`),
Example: heredoc.Doc(`
# create new bug label
$ gh label create bug --description "Something isn't working" --color E99695
`),
Args: cmdutil.ExactArgs(1, "cannot create label: name argument required"),
@ -81,8 +82,8 @@ func newCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Co
}
cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of the label")
cmd.Flags().StringVarP(&opts.Color, "color", "c", "", "Color of the label, if not specified one will be selected at random")
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Set the label color and description even if the name already exists.")
cmd.Flags().StringVarP(&opts.Color, "color", "c", "", "Color of the label")
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Update the label color and description if label already exists")
return cmd
}
@ -144,24 +145,42 @@ func createLabel(client *http.Client, repo ghrepo.Interface, opts *createOptions
}
if opts.Force && errors.Is(err, errLabelAlreadyExists) {
return updateLabel(apiClient, repo, opts)
editOpts := editOptions{
Description: opts.Description,
Color: opts.Color,
Name: opts.Name,
}
return updateLabel(apiClient, repo, &editOpts)
}
return err
}
func updateLabel(apiClient *api.Client, repo ghrepo.Interface, opts *createOptions) error {
func updateLabel(apiClient *api.Client, repo ghrepo.Interface, opts *editOptions) error {
path := fmt.Sprintf("repos/%s/%s/labels/%s", repo.RepoOwner(), repo.RepoName(), opts.Name)
requestByte, err := json.Marshal(map[string]string{
"description": opts.Description,
"color": opts.Color,
})
properties := map[string]string{}
if opts.Description != "" {
properties["description"] = opts.Description
}
if opts.Color != "" {
properties["color"] = opts.Color
}
if opts.NewName != "" {
properties["new_name"] = opts.NewName
}
requestByte, err := json.Marshal(properties)
if err != nil {
return err
}
requestBody := bytes.NewReader(requestByte)
result := label{}
return apiClient.REST(repo.RepoHost(), "PATCH", path, requestBody, &result)
err = apiClient.REST(repo.RepoHost(), "PATCH", path, requestBody, &result)
if httpError, ok := err.(api.HTTPError); ok && isLabelAlreadyExistsError(httpError) {
err = errLabelAlreadyExists
}
return err
}
func isLabelAlreadyExistsError(err api.HTTPError) bool {

104
pkg/cmd/label/edit.go Normal file
View file

@ -0,0 +1,104 @@
package label
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type editOptions struct {
BaseRepo func() (ghrepo.Interface, error)
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Color string
Description string
Name string
NewName string
}
func newCmdEdit(f *cmdutil.Factory, runF func(*editOptions) error) *cobra.Command {
opts := editOptions{
HttpClient: f.HttpClient,
IO: f.IOStreams,
}
cmd := &cobra.Command{
Use: "edit <name>",
Short: "Edit a label",
Long: heredoc.Docf(`
Update a label on GitHub.
A label can be renamed using the %[1]s--name%[1]s flag.
The label color needs to be 6 character hex value.
`, "`"),
Example: heredoc.Doc(`
# update the color of the bug label
$ gh label edit bug --color FF0000
# rename and edit the description of the bug label
$ gh label edit bug --name big-bug --description "Bigger than normal bug"
`),
Args: cmdutil.ExactArgs(1, "cannot update label: name argument required"),
RunE: func(c *cobra.Command, args []string) error {
opts.BaseRepo = f.BaseRepo
opts.Name = args[0]
opts.Color = strings.TrimPrefix(opts.Color, "#")
if opts.Description == "" &&
opts.Color == "" &&
opts.NewName == "" {
return cmdutil.FlagErrorf("specify at least one of `--color`, `--description`, or `--name`")
}
if runF != nil {
return runF(&opts)
}
return editRun(&opts)
},
}
cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of the label")
cmd.Flags().StringVarP(&opts.Color, "color", "c", "", "Color of the label")
cmd.Flags().StringVarP(&opts.NewName, "name", "n", "", "Name of the label")
return cmd
}
func editRun(opts *editOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
baseRepo, err := opts.BaseRepo()
if err != nil {
return err
}
opts.IO.StartProgressIndicator()
err = updateLabel(apiClient, baseRepo, opts)
opts.IO.StopProgressIndicator()
if err != nil {
if errors.Is(err, errLabelAlreadyExists) {
return fmt.Errorf("label with name %q already exists", opts.NewName)
}
return err
}
if opts.IO.IsStdoutTTY() {
cs := opts.IO.ColorScheme()
successMsg := fmt.Sprintf("%s Label %q updated in %s\n", cs.SuccessIcon(), opts.Name, ghrepo.FullName(baseRepo))
fmt.Fprint(opts.IO.Out, successMsg)
}
return nil
}

170
pkg/cmd/label/edit_test.go Normal file
View file

@ -0,0 +1,170 @@
package label
import (
"bytes"
"net/http"
"testing"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func TestNewCmdEdit(t *testing.T) {
tests := []struct {
name string
input string
output editOptions
wantErr bool
errMsg string
}{
{
name: "no argument",
input: "",
wantErr: true,
errMsg: "cannot update label: name argument required",
},
{
name: "name argument",
input: "test",
wantErr: true,
errMsg: "specify at least one of `--color`, `--description`, or `--name`",
},
{
name: "description flag",
input: "test --description 'some description'",
output: editOptions{Name: "test", Description: "some description"},
},
{
name: "color flag",
input: "test --color FFFFFF",
output: editOptions{Name: "test", Color: "FFFFFF"},
},
{
name: "color flag with pound sign",
input: "test --color '#AAAAAA'",
output: editOptions{Name: "test", Color: "AAAAAA"},
},
{
name: "name flag",
input: "test --name test1",
output: editOptions{Name: "test", NewName: "test1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
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.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantErr {
assert.EqualError(t, err, tt.errMsg)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.output.Color, gotOpts.Color)
assert.Equal(t, tt.output.Description, gotOpts.Description)
assert.Equal(t, tt.output.Name, gotOpts.Name)
assert.Equal(t, tt.output.NewName, gotOpts.NewName)
})
}
}
func TestEditRun(t *testing.T) {
tests := []struct {
name string
tty bool
opts *editOptions
httpStubs func(*httpmock.Registry)
wantStdout string
wantErrMsg string
}{
{
name: "updates label",
tty: true,
opts: &editOptions{Name: "test", Description: "some description"},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO/labels/test"),
httpmock.StatusStringResponse(201, "{}"),
)
},
wantStdout: "✓ Label \"test\" updated in OWNER/REPO\n",
},
{
name: "updates label notty",
tty: false,
opts: &editOptions{Name: "test", Description: "some description"},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO/labels/test"),
httpmock.StatusStringResponse(201, "{}"),
)
},
wantStdout: "",
},
{
name: "updates missing label",
opts: &editOptions{Name: "invalid", Description: "some description"},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO/labels/invalid"),
httpmock.WithHeader(
httpmock.StatusStringResponse(404, `{"message":"Not Found"}`),
"Content-Type",
"application/json",
),
)
},
wantErrMsg: "HTTP 404: Not Found (https://api.github.com/repos/OWNER/REPO/labels/invalid)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(reg)
}
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
io, _, stdout, _ := iostreams.Test()
io.SetStdoutTTY(tt.tty)
io.SetStdinTTY(tt.tty)
io.SetStderrTTY(tt.tty)
tt.opts.IO = io
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
}
defer reg.Verify(t)
err := editRun(tt.opts)
if tt.wantErrMsg != "" {
assert.EqualError(t, err, tt.wantErrMsg)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.wantStdout, stdout.String())
})
}
}

View file

@ -16,6 +16,7 @@ func NewCmdLabel(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(newCmdList(f, nil))
cmd.AddCommand(newCmdCreate(f, nil))
cmd.AddCommand(newCmdClone(f, nil))
cmd.AddCommand(newCmdEdit(f, nil))
return cmd
}