github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/label/create.go (about) 1 package label 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "math/rand" 9 "net/http" 10 "strings" 11 "time" 12 13 "github.com/MakeNowJust/heredoc" 14 "github.com/ungtb10d/cli/v2/api" 15 "github.com/ungtb10d/cli/v2/internal/ghrepo" 16 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 17 "github.com/ungtb10d/cli/v2/pkg/iostreams" 18 "github.com/spf13/cobra" 19 ) 20 21 var randomColors = []string{ 22 "B60205", 23 "D93F0B", 24 "FBCA04", 25 "0E8A16", 26 "006B75", 27 "1D76DB", 28 "0052CC", 29 "5319E7", 30 "E99695", 31 "F9D0C4", 32 "FEF2C0", 33 "C2E0C6", 34 "BFDADC", 35 "C5DEF5", 36 "BFD4F2", 37 "D4C5F9", 38 } 39 40 type createOptions struct { 41 BaseRepo func() (ghrepo.Interface, error) 42 HttpClient func() (*http.Client, error) 43 IO *iostreams.IOStreams 44 45 Color string 46 Description string 47 Name string 48 Force bool 49 } 50 51 func newCmdCreate(f *cmdutil.Factory, runF func(*createOptions) error) *cobra.Command { 52 opts := createOptions{ 53 HttpClient: f.HttpClient, 54 IO: f.IOStreams, 55 } 56 57 cmd := &cobra.Command{ 58 Use: "create <name>", 59 Short: "Create a new label", 60 Long: heredoc.Doc(` 61 Create a new label on GitHub, or updates an existing one with --force. 62 63 Must specify name for the label. The description and color are optional. 64 If a color isn't provided, a random one will be chosen. 65 66 The label color needs to be 6 character hex value. 67 `), 68 Example: heredoc.Doc(` 69 # create new bug label 70 $ gh label create bug --description "Something isn't working" --color E99695 71 `), 72 Args: cmdutil.ExactArgs(1, "cannot create label: name argument required"), 73 RunE: func(c *cobra.Command, args []string) error { 74 opts.BaseRepo = f.BaseRepo 75 opts.Name = args[0] 76 opts.Color = strings.TrimPrefix(opts.Color, "#") 77 if runF != nil { 78 return runF(&opts) 79 } 80 return createRun(&opts) 81 }, 82 } 83 84 cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Description of the label") 85 cmd.Flags().StringVarP(&opts.Color, "color", "c", "", "Color of the label") 86 cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Update the label color and description if label already exists") 87 88 return cmd 89 } 90 91 var errLabelAlreadyExists = errors.New("label already exists") 92 93 func createRun(opts *createOptions) error { 94 httpClient, err := opts.HttpClient() 95 if err != nil { 96 return err 97 } 98 99 baseRepo, err := opts.BaseRepo() 100 if err != nil { 101 return err 102 } 103 104 if opts.Color == "" { 105 rand.Seed(time.Now().UnixNano()) 106 opts.Color = randomColors[rand.Intn(len(randomColors)-1)] 107 } 108 109 opts.IO.StartProgressIndicator() 110 err = createLabel(httpClient, baseRepo, opts) 111 opts.IO.StopProgressIndicator() 112 if err != nil { 113 if errors.Is(err, errLabelAlreadyExists) { 114 return fmt.Errorf("label with name %q already exists; use `--force` to update its color and description", opts.Name) 115 } 116 return err 117 } 118 119 if opts.IO.IsStdoutTTY() { 120 cs := opts.IO.ColorScheme() 121 successMsg := fmt.Sprintf("%s Label %q created in %s\n", cs.SuccessIcon(), opts.Name, ghrepo.FullName(baseRepo)) 122 fmt.Fprint(opts.IO.Out, successMsg) 123 } 124 125 return nil 126 } 127 128 func createLabel(client *http.Client, repo ghrepo.Interface, opts *createOptions) error { 129 apiClient := api.NewClientFromHTTP(client) 130 path := fmt.Sprintf("repos/%s/%s/labels", repo.RepoOwner(), repo.RepoName()) 131 requestByte, err := json.Marshal(map[string]string{ 132 "name": opts.Name, 133 "description": opts.Description, 134 "color": opts.Color, 135 }) 136 if err != nil { 137 return err 138 } 139 requestBody := bytes.NewReader(requestByte) 140 result := label{} 141 err = apiClient.REST(repo.RepoHost(), "POST", path, requestBody, &result) 142 143 if httpError, ok := err.(api.HTTPError); ok && isLabelAlreadyExistsError(httpError) { 144 err = errLabelAlreadyExists 145 } 146 147 if opts.Force && errors.Is(err, errLabelAlreadyExists) { 148 editOpts := editOptions{ 149 Description: opts.Description, 150 Color: opts.Color, 151 Name: opts.Name, 152 } 153 return updateLabel(apiClient, repo, &editOpts) 154 } 155 156 return err 157 } 158 159 func updateLabel(apiClient *api.Client, repo ghrepo.Interface, opts *editOptions) error { 160 path := fmt.Sprintf("repos/%s/%s/labels/%s", repo.RepoOwner(), repo.RepoName(), opts.Name) 161 properties := map[string]string{} 162 if opts.Description != "" { 163 properties["description"] = opts.Description 164 } 165 if opts.Color != "" { 166 properties["color"] = opts.Color 167 } 168 if opts.NewName != "" { 169 properties["new_name"] = opts.NewName 170 } 171 requestByte, err := json.Marshal(properties) 172 if err != nil { 173 return err 174 } 175 requestBody := bytes.NewReader(requestByte) 176 result := label{} 177 err = apiClient.REST(repo.RepoHost(), "PATCH", path, requestBody, &result) 178 179 if httpError, ok := err.(api.HTTPError); ok && isLabelAlreadyExistsError(httpError) { 180 err = errLabelAlreadyExists 181 } 182 183 return err 184 } 185 186 func isLabelAlreadyExistsError(err api.HTTPError) bool { 187 return err.StatusCode == 422 && len(err.Errors) == 1 && err.Errors[0].Field == "name" && err.Errors[0].Code == "already_exists" 188 }