label: add list/create/edit/delete/palette, hint from issue labelUnverified
Labels are addressed by name (ids are UUIDs). The server only accepts colors from a fixed palette, so the CLI prints it on invalid_color and exposes it as `label palette`. issue label points at label create when a name does not exist. Fixes #3 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgkcT3UPxwxukBKojB5eg4
1b60774 parent: 72b5c57 modified
cmd/issue.go +9 -0 | @@ -1,10 +1,13 @@ | ||
| 1 | 1 | package cmd |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | + "errors" | |
| 4 | 5 | "fmt" |
| 5 | 6 | "strconv" |
| 6 | 7 | "strings" |
| 7 | 8 | |
| 9 | + "rickub.com/rickub/cli/internal/api" | |
| 10 | + | |
| 8 | 11 | "github.com/spf13/cobra" |
| 9 | 12 | ) |
| 10 | 13 | |
| @@ -346,6 +349,12 @@ func runIssueLabel(cmd *cobra.Command, args []string) error { | ||
| 346 | 349 | names = labelNames(issueLabels) |
| 347 | 350 | } |
| 348 | 351 | if err := client.SetIssueLabels(cmd.Context(), owner, repo, n, names); err != nil { |
| 352 | + // Not %w: main prints a bare APIError it can unwrap, which would drop | |
| 353 | + // the hint. | |
| 354 | + var apiErr *api.APIError | |
| 355 | + if errors.As(err, &apiErr) && apiErr.Code == "no_such_label" { | |
| 356 | + return fmt.Errorf("%s — create it first with `rickub label create <name> -R %s/%s`", err, owner, repo) | |
| 357 | + } | |
| 349 | 358 | return err |
| 350 | 359 | } |
| 351 | 360 | if len(names) == 0 { |
| @@ -1,10 +1,13 @@ | |||
| 1 | package cmd | 1 | package cmd |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | + "errors" | ||
| 4 | "fmt" | 5 | "fmt" |
| 5 | "strconv" | 6 | "strconv" |
| 6 | "strings" | 7 | "strings" |
| 7 | 8 | ||
| 9 | + "rickub.com/rickub/cli/internal/api" | ||
| 10 | + | ||
| 8 | "github.com/spf13/cobra" | 11 | "github.com/spf13/cobra" |
| 9 | ) | 12 | ) |
| 10 | 13 | ||
| @@ -346,6 +349,12 @@ func runIssueLabel(cmd *cobra.Command, args []string) error { | |||
| 346 | names = labelNames(issueLabels) | 349 | names = labelNames(issueLabels) |
| 347 | } | 350 | } |
| 348 | if err := client.SetIssueLabels(cmd.Context(), owner, repo, n, names); err != nil { | 351 | if err := client.SetIssueLabels(cmd.Context(), owner, repo, n, names); err != nil { |
| 352 | + // Not %w: main prints a bare APIError it can unwrap, which would drop | ||
| 353 | + // the hint. | ||
| 354 | + var apiErr *api.APIError | ||
| 355 | + if errors.As(err, &apiErr) && apiErr.Code == "no_such_label" { | ||
| 356 | + return fmt.Errorf("%s — create it first with `rickub label create <name> -R %s/%s`", err, owner, repo) | ||
| 357 | + } | ||
| 349 | return err | 358 | return err |
| 350 | } | 359 | } |
| 351 | if len(names) == 0 { | 360 | if len(names) == 0 { |
added
cmd/label.go +224 -0 | new file mode 100644 | ||
| @@ -0,0 +1,224 @@ | ||
| 1 | +package cmd | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "errors" | |
| 5 | + "fmt" | |
| 6 | + "strings" | |
| 7 | + | |
| 8 | + "rickub.com/rickub/cli/internal/api" | |
| 9 | + | |
| 10 | + "github.com/spf13/cobra" | |
| 11 | +) | |
| 12 | + | |
| 13 | +var ( | |
| 14 | + labelRepo string | |
| 15 | + labelColor string | |
| 16 | + labelNewName string | |
| 17 | +) | |
| 18 | + | |
| 19 | +func init() { | |
| 20 | + labelCmd := &cobra.Command{ | |
| 21 | + Use: "label", | |
| 22 | + Short: "Manage a repository's labels", | |
| 23 | + } | |
| 24 | + labelCmd.PersistentFlags().StringVarP(&labelRepo, "repo", "R", "", "target repo as owner/repo (default: current git remote)") | |
| 25 | + | |
| 26 | + listCmd := &cobra.Command{ | |
| 27 | + Use: "list", | |
| 28 | + Short: "List the repo's labels", | |
| 29 | + Args: cobra.NoArgs, | |
| 30 | + RunE: runLabelList, | |
| 31 | + } | |
| 32 | + | |
| 33 | + createCmd := &cobra.Command{ | |
| 34 | + Use: "create <name>", | |
| 35 | + Short: "Create a label", | |
| 36 | + Args: cobra.ExactArgs(1), | |
| 37 | + RunE: runLabelCreate, | |
| 38 | + } | |
| 39 | + createCmd.Flags().StringVar(&labelColor, "color", "", "one of the palette colors (see `rickub label palette`; server default when omitted)") | |
| 40 | + | |
| 41 | + editCmd := &cobra.Command{ | |
| 42 | + Use: "edit <name>", | |
| 43 | + Short: "Rename or recolor a label", | |
| 44 | + Args: cobra.ExactArgs(1), | |
| 45 | + RunE: runLabelEdit, | |
| 46 | + } | |
| 47 | + editCmd.Flags().StringVar(&labelNewName, "name", "", "new name") | |
| 48 | + editCmd.Flags().StringVar(&labelColor, "color", "", "new color, from the palette") | |
| 49 | + | |
| 50 | + deleteCmd := &cobra.Command{ | |
| 51 | + Use: "delete <name>", | |
| 52 | + Short: "Delete a label", | |
| 53 | + Args: cobra.ExactArgs(1), | |
| 54 | + RunE: runLabelDelete, | |
| 55 | + } | |
| 56 | + | |
| 57 | + paletteCmd := &cobra.Command{ | |
| 58 | + Use: "palette", | |
| 59 | + Short: "Print the label colors the server accepts", | |
| 60 | + Args: cobra.NoArgs, | |
| 61 | + Run: func(cmd *cobra.Command, _ []string) { | |
| 62 | + for _, c := range labelPalette { | |
| 63 | + fmt.Fprintln(cmd.OutOrStdout(), c) | |
| 64 | + } | |
| 65 | + }, | |
| 66 | + } | |
| 67 | + | |
| 68 | + labelCmd.AddCommand(listCmd, createCmd, editCmd, deleteCmd, paletteCmd) | |
| 69 | + rootCmd.AddCommand(labelCmd) | |
| 70 | +} | |
| 71 | + | |
| 72 | +// labelPalette mirrors the server's fixed label palette (6-hex, no '#'): any | |
| 73 | +// other color is refused with invalid_color, so the CLI can say so up front. | |
| 74 | +var labelPalette = []string{"b60205", "d73a4a", "e99695", "fbca04", "fef2c0", "0e8a16", "c2e0c6", "006b75", "bfdadc", "1d76db", "0052cc", "5319e7", "d4c5f9", "e4e669", "ededed", "f9d0c4"} | |
| 75 | + | |
| 76 | +// colorError turns the server's invalid_color refusal into one that lists | |
| 77 | +// what would have been accepted. | |
| 78 | +func colorError(err error) error { | |
| 79 | + var apiErr *api.APIError | |
| 80 | + if errors.As(err, &apiErr) && apiErr.Code == "invalid_color" { | |
| 81 | + return fmt.Errorf("%s — accepted colors: %s", err, strings.Join(labelPalette, ", ")) | |
| 82 | + } | |
| 83 | + return err | |
| 84 | +} | |
| 85 | + | |
| 86 | +func runLabelList(cmd *cobra.Command, _ []string) error { | |
| 87 | + client, err := newClient() | |
| 88 | + if err != nil { | |
| 89 | + return err | |
| 90 | + } | |
| 91 | + owner, repo, err := resolveRepo(labelRepo) | |
| 92 | + if err != nil { | |
| 93 | + return err | |
| 94 | + } | |
| 95 | + labels, err := client.ListLabels(cmd.Context(), owner, repo) | |
| 96 | + if err != nil { | |
| 97 | + return err | |
| 98 | + } | |
| 99 | + return printLabels(cmd, labels) | |
| 100 | +} | |
| 101 | + | |
| 102 | +func printLabels(cmd *cobra.Command, labels []api.Label) error { | |
| 103 | + if flagJSON { | |
| 104 | + return printJSON(cmd.OutOrStdout(), labels) | |
| 105 | + } | |
| 106 | + if len(labels) == 0 { | |
| 107 | + fmt.Fprintln(cmd.OutOrStdout(), "No labels.") | |
| 108 | + return nil | |
| 109 | + } | |
| 110 | + tw := newTabw(cmd.OutOrStdout()) | |
| 111 | + fmt.Fprintln(tw, "NAME\tCOLOR") | |
| 112 | + for _, l := range labels { | |
| 113 | + fmt.Fprintf(tw, "%s\t%s\n", l.Name, dash(l.Color)) | |
| 114 | + } | |
| 115 | + return tw.Flush() | |
| 116 | +} | |
| 117 | + | |
| 118 | +func runLabelCreate(cmd *cobra.Command, args []string) error { | |
| 119 | + client, err := newClient() | |
| 120 | + if err != nil { | |
| 121 | + return err | |
| 122 | + } | |
| 123 | + owner, repo, err := resolveRepo(labelRepo) | |
| 124 | + if err != nil { | |
| 125 | + return err | |
| 126 | + } | |
| 127 | + name := strings.TrimSpace(args[0]) | |
| 128 | + if name == "" { | |
| 129 | + return fmt.Errorf("label name must not be empty") | |
| 130 | + } | |
| 131 | + l, err := client.CreateLabel(cmd.Context(), owner, repo, name, normalizeColor(labelColor)) | |
| 132 | + if err != nil { | |
| 133 | + return colorError(err) | |
| 134 | + } | |
| 135 | + if flagJSON { | |
| 136 | + return printJSON(cmd.OutOrStdout(), l) | |
| 137 | + } | |
| 138 | + fmt.Fprintf(cmd.OutOrStdout(), "Created label %s (%s)\n", l.Name, dash(l.Color)) | |
| 139 | + return nil | |
| 140 | +} | |
| 141 | + | |
| 142 | +func runLabelEdit(cmd *cobra.Command, args []string) error { | |
| 143 | + if labelNewName == "" && labelColor == "" { | |
| 144 | + return fmt.Errorf("nothing to change: pass --name and/or --color") | |
| 145 | + } | |
| 146 | + client, err := newClient() | |
| 147 | + if err != nil { | |
| 148 | + return err | |
| 149 | + } | |
| 150 | + owner, repo, err := resolveRepo(labelRepo) | |
| 151 | + if err != nil { | |
| 152 | + return err | |
| 153 | + } | |
| 154 | + l, err := findLabel(cmd, client, owner, repo, args[0]) | |
| 155 | + if err != nil { | |
| 156 | + return err | |
| 157 | + } | |
| 158 | + patch := map[string]any{} | |
| 159 | + if labelNewName != "" { | |
| 160 | + patch["name"] = labelNewName | |
| 161 | + } | |
| 162 | + if labelColor != "" { | |
| 163 | + patch["color"] = normalizeColor(labelColor) | |
| 164 | + } | |
| 165 | + updated, err := client.UpdateLabel(cmd.Context(), owner, repo, l.ID, patch) | |
| 166 | + if err != nil { | |
| 167 | + return colorError(err) | |
| 168 | + } | |
| 169 | + if flagJSON { | |
| 170 | + return printJSON(cmd.OutOrStdout(), updated) | |
| 171 | + } | |
| 172 | + fmt.Fprintf(cmd.OutOrStdout(), "Updated label %s (%s)\n", updated.Name, dash(updated.Color)) | |
| 173 | + return nil | |
| 174 | +} | |
| 175 | + | |
| 176 | +func runLabelDelete(cmd *cobra.Command, args []string) error { | |
| 177 | + client, err := newClient() | |
| 178 | + if err != nil { | |
| 179 | + return err | |
| 180 | + } | |
| 181 | + owner, repo, err := resolveRepo(labelRepo) | |
| 182 | + if err != nil { | |
| 183 | + return err | |
| 184 | + } | |
| 185 | + l, err := findLabel(cmd, client, owner, repo, args[0]) | |
| 186 | + if err != nil { | |
| 187 | + return err | |
| 188 | + } | |
| 189 | + if err := client.DeleteLabel(cmd.Context(), owner, repo, l.ID); err != nil { | |
| 190 | + return err | |
| 191 | + } | |
| 192 | + fmt.Fprintf(cmd.OutOrStdout(), "Deleted label %s\n", l.Name) | |
| 193 | + return nil | |
| 194 | +} | |
| 195 | + | |
| 196 | +// findLabel resolves a label by exact name (falling back to a case-insensitive | |
| 197 | +// match when unambiguous); labels are addressed by name on the command line | |
| 198 | +// because ids are opaque UUIDs. | |
| 199 | +func findLabel(cmd *cobra.Command, client *api.Client, owner, repo, name string) (*api.Label, error) { | |
| 200 | + labels, err := client.ListLabels(cmd.Context(), owner, repo) | |
| 201 | + if err != nil { | |
| 202 | + return nil, err | |
| 203 | + } | |
| 204 | + var loose *api.Label | |
| 205 | + looseCount := 0 | |
| 206 | + for i := range labels { | |
| 207 | + if labels[i].Name == name { | |
| 208 | + return &labels[i], nil | |
| 209 | + } | |
| 210 | + if strings.EqualFold(labels[i].Name, name) { | |
| 211 | + loose = &labels[i] | |
| 212 | + looseCount++ | |
| 213 | + } | |
| 214 | + } | |
| 215 | + if looseCount == 1 { | |
| 216 | + return loose, nil | |
| 217 | + } | |
| 218 | + return nil, fmt.Errorf("no label named %q in %s/%s (see `rickub label list -R %s/%s`)", name, owner, repo, owner, repo) | |
| 219 | +} | |
| 220 | + | |
| 221 | +// normalizeColor strips a leading '#' so both "d73a4a" and "#d73a4a" work. | |
| 222 | +func normalizeColor(c string) string { | |
| 223 | + return strings.TrimPrefix(strings.TrimSpace(c), "#") | |
| 224 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,224 @@ | |||
| 1 | +package cmd | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "errors" | ||
| 5 | + "fmt" | ||
| 6 | + "strings" | ||
| 7 | + | ||
| 8 | + "rickub.com/rickub/cli/internal/api" | ||
| 9 | + | ||
| 10 | + "github.com/spf13/cobra" | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +var ( | ||
| 14 | + labelRepo string | ||
| 15 | + labelColor string | ||
| 16 | + labelNewName string | ||
| 17 | +) | ||
| 18 | + | ||
| 19 | +func init() { | ||
| 20 | + labelCmd := &cobra.Command{ | ||
| 21 | + Use: "label", | ||
| 22 | + Short: "Manage a repository's labels", | ||
| 23 | + } | ||
| 24 | + labelCmd.PersistentFlags().StringVarP(&labelRepo, "repo", "R", "", "target repo as owner/repo (default: current git remote)") | ||
| 25 | + | ||
| 26 | + listCmd := &cobra.Command{ | ||
| 27 | + Use: "list", | ||
| 28 | + Short: "List the repo's labels", | ||
| 29 | + Args: cobra.NoArgs, | ||
| 30 | + RunE: runLabelList, | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + createCmd := &cobra.Command{ | ||
| 34 | + Use: "create <name>", | ||
| 35 | + Short: "Create a label", | ||
| 36 | + Args: cobra.ExactArgs(1), | ||
| 37 | + RunE: runLabelCreate, | ||
| 38 | + } | ||
| 39 | + createCmd.Flags().StringVar(&labelColor, "color", "", "one of the palette colors (see `rickub label palette`; server default when omitted)") | ||
| 40 | + | ||
| 41 | + editCmd := &cobra.Command{ | ||
| 42 | + Use: "edit <name>", | ||
| 43 | + Short: "Rename or recolor a label", | ||
| 44 | + Args: cobra.ExactArgs(1), | ||
| 45 | + RunE: runLabelEdit, | ||
| 46 | + } | ||
| 47 | + editCmd.Flags().StringVar(&labelNewName, "name", "", "new name") | ||
| 48 | + editCmd.Flags().StringVar(&labelColor, "color", "", "new color, from the palette") | ||
| 49 | + | ||
| 50 | + deleteCmd := &cobra.Command{ | ||
| 51 | + Use: "delete <name>", | ||
| 52 | + Short: "Delete a label", | ||
| 53 | + Args: cobra.ExactArgs(1), | ||
| 54 | + RunE: runLabelDelete, | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + paletteCmd := &cobra.Command{ | ||
| 58 | + Use: "palette", | ||
| 59 | + Short: "Print the label colors the server accepts", | ||
| 60 | + Args: cobra.NoArgs, | ||
| 61 | + Run: func(cmd *cobra.Command, _ []string) { | ||
| 62 | + for _, c := range labelPalette { | ||
| 63 | + fmt.Fprintln(cmd.OutOrStdout(), c) | ||
| 64 | + } | ||
| 65 | + }, | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + labelCmd.AddCommand(listCmd, createCmd, editCmd, deleteCmd, paletteCmd) | ||
| 69 | + rootCmd.AddCommand(labelCmd) | ||
| 70 | +} | ||
| 71 | + | ||
| 72 | +// labelPalette mirrors the server's fixed label palette (6-hex, no '#'): any | ||
| 73 | +// other color is refused with invalid_color, so the CLI can say so up front. | ||
| 74 | +var labelPalette = []string{"b60205", "d73a4a", "e99695", "fbca04", "fef2c0", "0e8a16", "c2e0c6", "006b75", "bfdadc", "1d76db", "0052cc", "5319e7", "d4c5f9", "e4e669", "ededed", "f9d0c4"} | ||
| 75 | + | ||
| 76 | +// colorError turns the server's invalid_color refusal into one that lists | ||
| 77 | +// what would have been accepted. | ||
| 78 | +func colorError(err error) error { | ||
| 79 | + var apiErr *api.APIError | ||
| 80 | + if errors.As(err, &apiErr) && apiErr.Code == "invalid_color" { | ||
| 81 | + return fmt.Errorf("%s — accepted colors: %s", err, strings.Join(labelPalette, ", ")) | ||
| 82 | + } | ||
| 83 | + return err | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +func runLabelList(cmd *cobra.Command, _ []string) error { | ||
| 87 | + client, err := newClient() | ||
| 88 | + if err != nil { | ||
| 89 | + return err | ||
| 90 | + } | ||
| 91 | + owner, repo, err := resolveRepo(labelRepo) | ||
| 92 | + if err != nil { | ||
| 93 | + return err | ||
| 94 | + } | ||
| 95 | + labels, err := client.ListLabels(cmd.Context(), owner, repo) | ||
| 96 | + if err != nil { | ||
| 97 | + return err | ||
| 98 | + } | ||
| 99 | + return printLabels(cmd, labels) | ||
| 100 | +} | ||
| 101 | + | ||
| 102 | +func printLabels(cmd *cobra.Command, labels []api.Label) error { | ||
| 103 | + if flagJSON { | ||
| 104 | + return printJSON(cmd.OutOrStdout(), labels) | ||
| 105 | + } | ||
| 106 | + if len(labels) == 0 { | ||
| 107 | + fmt.Fprintln(cmd.OutOrStdout(), "No labels.") | ||
| 108 | + return nil | ||
| 109 | + } | ||
| 110 | + tw := newTabw(cmd.OutOrStdout()) | ||
| 111 | + fmt.Fprintln(tw, "NAME\tCOLOR") | ||
| 112 | + for _, l := range labels { | ||
| 113 | + fmt.Fprintf(tw, "%s\t%s\n", l.Name, dash(l.Color)) | ||
| 114 | + } | ||
| 115 | + return tw.Flush() | ||
| 116 | +} | ||
| 117 | + | ||
| 118 | +func runLabelCreate(cmd *cobra.Command, args []string) error { | ||
| 119 | + client, err := newClient() | ||
| 120 | + if err != nil { | ||
| 121 | + return err | ||
| 122 | + } | ||
| 123 | + owner, repo, err := resolveRepo(labelRepo) | ||
| 124 | + if err != nil { | ||
| 125 | + return err | ||
| 126 | + } | ||
| 127 | + name := strings.TrimSpace(args[0]) | ||
| 128 | + if name == "" { | ||
| 129 | + return fmt.Errorf("label name must not be empty") | ||
| 130 | + } | ||
| 131 | + l, err := client.CreateLabel(cmd.Context(), owner, repo, name, normalizeColor(labelColor)) | ||
| 132 | + if err != nil { | ||
| 133 | + return colorError(err) | ||
| 134 | + } | ||
| 135 | + if flagJSON { | ||
| 136 | + return printJSON(cmd.OutOrStdout(), l) | ||
| 137 | + } | ||
| 138 | + fmt.Fprintf(cmd.OutOrStdout(), "Created label %s (%s)\n", l.Name, dash(l.Color)) | ||
| 139 | + return nil | ||
| 140 | +} | ||
| 141 | + | ||
| 142 | +func runLabelEdit(cmd *cobra.Command, args []string) error { | ||
| 143 | + if labelNewName == "" && labelColor == "" { | ||
| 144 | + return fmt.Errorf("nothing to change: pass --name and/or --color") | ||
| 145 | + } | ||
| 146 | + client, err := newClient() | ||
| 147 | + if err != nil { | ||
| 148 | + return err | ||
| 149 | + } | ||
| 150 | + owner, repo, err := resolveRepo(labelRepo) | ||
| 151 | + if err != nil { | ||
| 152 | + return err | ||
| 153 | + } | ||
| 154 | + l, err := findLabel(cmd, client, owner, repo, args[0]) | ||
| 155 | + if err != nil { | ||
| 156 | + return err | ||
| 157 | + } | ||
| 158 | + patch := map[string]any{} | ||
| 159 | + if labelNewName != "" { | ||
| 160 | + patch["name"] = labelNewName | ||
| 161 | + } | ||
| 162 | + if labelColor != "" { | ||
| 163 | + patch["color"] = normalizeColor(labelColor) | ||
| 164 | + } | ||
| 165 | + updated, err := client.UpdateLabel(cmd.Context(), owner, repo, l.ID, patch) | ||
| 166 | + if err != nil { | ||
| 167 | + return colorError(err) | ||
| 168 | + } | ||
| 169 | + if flagJSON { | ||
| 170 | + return printJSON(cmd.OutOrStdout(), updated) | ||
| 171 | + } | ||
| 172 | + fmt.Fprintf(cmd.OutOrStdout(), "Updated label %s (%s)\n", updated.Name, dash(updated.Color)) | ||
| 173 | + return nil | ||
| 174 | +} | ||
| 175 | + | ||
| 176 | +func runLabelDelete(cmd *cobra.Command, args []string) error { | ||
| 177 | + client, err := newClient() | ||
| 178 | + if err != nil { | ||
| 179 | + return err | ||
| 180 | + } | ||
| 181 | + owner, repo, err := resolveRepo(labelRepo) | ||
| 182 | + if err != nil { | ||
| 183 | + return err | ||
| 184 | + } | ||
| 185 | + l, err := findLabel(cmd, client, owner, repo, args[0]) | ||
| 186 | + if err != nil { | ||
| 187 | + return err | ||
| 188 | + } | ||
| 189 | + if err := client.DeleteLabel(cmd.Context(), owner, repo, l.ID); err != nil { | ||
| 190 | + return err | ||
| 191 | + } | ||
| 192 | + fmt.Fprintf(cmd.OutOrStdout(), "Deleted label %s\n", l.Name) | ||
| 193 | + return nil | ||
| 194 | +} | ||
| 195 | + | ||
| 196 | +// findLabel resolves a label by exact name (falling back to a case-insensitive | ||
| 197 | +// match when unambiguous); labels are addressed by name on the command line | ||
| 198 | +// because ids are opaque UUIDs. | ||
| 199 | +func findLabel(cmd *cobra.Command, client *api.Client, owner, repo, name string) (*api.Label, error) { | ||
| 200 | + labels, err := client.ListLabels(cmd.Context(), owner, repo) | ||
| 201 | + if err != nil { | ||
| 202 | + return nil, err | ||
| 203 | + } | ||
| 204 | + var loose *api.Label | ||
| 205 | + looseCount := 0 | ||
| 206 | + for i := range labels { | ||
| 207 | + if labels[i].Name == name { | ||
| 208 | + return &labels[i], nil | ||
| 209 | + } | ||
| 210 | + if strings.EqualFold(labels[i].Name, name) { | ||
| 211 | + loose = &labels[i] | ||
| 212 | + looseCount++ | ||
| 213 | + } | ||
| 214 | + } | ||
| 215 | + if looseCount == 1 { | ||
| 216 | + return loose, nil | ||
| 217 | + } | ||
| 218 | + return nil, fmt.Errorf("no label named %q in %s/%s (see `rickub label list -R %s/%s`)", name, owner, repo, owner, repo) | ||
| 219 | +} | ||
| 220 | + | ||
| 221 | +// normalizeColor strips a leading '#' so both "d73a4a" and "#d73a4a" work. | ||
| 222 | +func normalizeColor(c string) string { | ||
| 223 | + return strings.TrimPrefix(strings.TrimSpace(c), "#") | ||
| 224 | +} | ||