diff --git a/pkg/cmd/label/create/create.go b/pkg/cmd/label/create/create.go index 9fc38bf0d..581cf5234 100644 --- a/pkg/cmd/label/create/create.go +++ b/pkg/cmd/label/create/create.go @@ -3,6 +3,7 @@ package create import ( "bytes" "encoding/json" + "errors" "fmt" "math/rand" "net/http" @@ -45,6 +46,7 @@ type CreateOptions struct { Color string Description string Name string + Force bool } func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { @@ -81,10 +83,13 @@ 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.") return cmd } +var errLabelAlreadyExists = errors.New("label already exists") + func createRun(opts *CreateOptions) error { httpClient, err := opts.HttpClient() if err != nil { @@ -105,12 +110,15 @@ func createRun(opts *CreateOptions) error { err = createLabel(httpClient, baseRepo, opts) opts.IO.StopProgressIndicator() if err != nil { + if errors.Is(err, errLabelAlreadyExists) { + return fmt.Errorf("label with name %q already exists; use `--force` to update its color and description", opts.Name) + } return err } if opts.IO.IsStdoutTTY() { cs := opts.IO.ColorScheme() - successMsg := fmt.Sprintf("\n%s Label %q created in %s\n", cs.SuccessIcon(), opts.Name, ghrepo.FullName(baseRepo)) + successMsg := fmt.Sprintf("%s Label %q created in %s\n", cs.SuccessIcon(), opts.Name, ghrepo.FullName(baseRepo)) fmt.Fprint(opts.IO.Out, successMsg) } @@ -130,5 +138,33 @@ func createLabel(client *http.Client, repo ghrepo.Interface, opts *CreateOptions } requestBody := bytes.NewReader(requestByte) result := shared.Label{} - return apiClient.REST(repo.RepoHost(), "POST", path, requestBody, &result) + err = apiClient.REST(repo.RepoHost(), "POST", path, requestBody, &result) + + if httpError, ok := err.(api.HTTPError); ok && isLabelAlreadyExistsError(httpError) { + err = errLabelAlreadyExists + } + + if opts.Force && errors.Is(err, errLabelAlreadyExists) { + return updateLabel(apiClient, repo, opts) + } + + return err +} + +func updateLabel(apiClient *api.Client, repo ghrepo.Interface, opts *CreateOptions) 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, + }) + if err != nil { + return err + } + requestBody := bytes.NewReader(requestByte) + result := shared.Label{} + return apiClient.REST(repo.RepoHost(), "PATCH", path, requestBody, &result) +} + +func isLabelAlreadyExistsError(err api.HTTPError) bool { + return err.StatusCode == 422 && len(err.Errors) == 1 && err.Errors[0].Field == "name" && err.Errors[0].Code == "already_exists" } diff --git a/pkg/cmd/label/create/create_test.go b/pkg/cmd/label/create/create_test.go index be40563a1..2d92f7a6f 100644 --- a/pkg/cmd/label/create/create_test.go +++ b/pkg/cmd/label/create/create_test.go @@ -101,7 +101,7 @@ func TestCreateRun(t *testing.T) { httpmock.StatusStringResponse(201, "{}"), ) }, - wantStdout: "\nāœ“ Label \"test\" created in OWNER/REPO\n", + wantStdout: "āœ“ Label \"test\" created in OWNER/REPO\n", }, { name: "creates label notty", @@ -115,6 +115,24 @@ func TestCreateRun(t *testing.T) { }, wantStdout: "", }, + { + name: "creates existing label", + opts: &CreateOptions{Name: "test", Description: "some description", Force: true}, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/labels"), + httpmock.WithHeader( + httpmock.StatusStringResponse(422, `{"message":"Validation Failed","errors":[{"resource":"Label","code":"already_exists","field":"name"}]}`), + "Content-Type", + "application/json", + ), + ) + reg.Register( + httpmock.REST("PATCH", "repos/OWNER/REPO/labels/test"), + httpmock.StatusStringResponse(201, "{}"), + ) + }, + }, } for _, tt := range tests {