internal/control/label.go
141 lines · 4288 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "regexp"
8 "strings"
9
10 "gitbay.org/gitbay/internal/policy"
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15func init() {
16 register(Command{Path: []string{"label", "list"},
17 Summary: "list a repository's labels with colour and use",
18 Usage: "label list <owner/name>",
19 Examples: []string{"label list krz/gitbay"},
20 ReadOnly: true, Run: runLabelList})
21 register(Command{Path: []string{"label", "set"},
22 Summary: "create a label or set its colour",
23 Usage: "label set <owner/name> <label> [--color rrggbb|'']",
24 Flags: []Flag{
25 {"--color", "rrggbb|''", "the label's colour, or '' to clear it", ""},
26 },
27 Examples: []string{"label set krz/gitbay bug --color d73a4a"},
28 Run: runLabelSet})
29 register(Command{Path: []string{"label", "remove"},
30 Summary: "remove a label from the repository and from every issue and merge request",
31 Usage: "label remove <owner/name> <label>",
32 Examples: []string{"label remove krz/gitbay wontfix"},
33 Run: runLabelRemove})
34}
35
36// A colour is six hex digits, with or without the hash: over bare ssh a
37// bare # starts a comment for the remote tokenizer, so the form without
38// it is the one that types cleanly there.
39var labelColorPat = regexp.MustCompile(`^#?[0-9a-fA-F]{6}$`)
40
41func runLabelList(c *Ctx, args []string) int {
42 if len(args) != 1 {
43 return c.usage()
44 }
45 repo, code := resolveRepo(c, args[0], policy.CanRead)
46 if code >= 0 {
47 return code
48 }
49 readable, err := ReadableScope(c.Store, c.User, repo)
50 if err != nil {
51 return c.fail(protocol.ExitFailure, "%v", err)
52 }
53 labels, err := c.Store.ListLabels(repo, readable)
54 if err != nil {
55 return c.fail(protocol.ExitFailure, "%v", err)
56 }
57 return c.emit(labels, func(w io.Writer) {
58 tb := c.table(w, "NAME", "COLOR", "ISSUES", "MRS")
59 for _, l := range labels {
60 cells := []cell{cRef(l.Name), cText(l.Color), cNum(l.Issues), cNum(l.MRs)}
61 if l.Org {
62 cells = append(cells, cText("org"))
63 }
64 tb.row(cells...)
65 }
66 tb.flush()
67 })
68}
69
70func runLabelSet(c *Ctx, args []string) int {
71 f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: -1, Usage: c.Cmd.Usage})
72 if err != nil {
73 return c.fail(protocol.ExitUsage, "%v", err)
74 }
75 rest := f.Pos
76 color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color")
77 if len(rest) != 2 {
78 return c.usage()
79 }
80 if colorSet && color != "" {
81 if !labelColorPat.MatchString(color) {
82 return c.fail(protocol.ExitUsage, "--color takes rrggbb (with or without #), or '' to clear")
83 }
84 color = "#" + strings.TrimPrefix(color, "#")
85 }
86 repo, code := resolveRepo(c, rest[0], policy.CanWrite)
87 if code >= 0 {
88 return code
89 }
90 if code := refuseArchived(c, repo); code >= 0 {
91 return code
92 }
93 name := rest[1]
94 if name == "" || len(name) > 50 {
95 return c.fail(protocol.ExitUsage, "a label is 1 to 50 characters")
96 }
97 if !colorSet {
98 // Keep the colour it has, if any; this is "make sure it exists".
99 if l, err := c.Store.LabelByName(repo, name); err == nil && !l.Org {
100 color = l.Color
101 }
102 }
103 if err := c.Store.SetLabel(repo, name, color); err != nil {
104 if errors.Is(err, store.ErrOrgScoped) {
105 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "label", name, "set"))
106 }
107 return c.fail(protocol.ExitFailure, "%v", err)
108 }
109 return c.emit(store.Label{Name: name, Color: color}, func(w io.Writer) {
110 if color == "" {
111 fmt.Fprintf(w, "label %s, no colour set\n", name)
112 return
113 }
114 fmt.Fprintf(w, "label %s is %s\n", name, color)
115 })
116}
117
118func runLabelRemove(c *Ctx, args []string) int {
119 if len(args) != 2 {
120 return c.usage()
121 }
122 repo, code := resolveRepo(c, args[0], policy.CanWrite)
123 if code >= 0 {
124 return code
125 }
126 if code := refuseArchived(c, repo); code >= 0 {
127 return code
128 }
129 if err := c.Store.DeleteLabel(repo, args[1]); err != nil {
130 if errors.Is(err, store.ErrOrgScoped) {
131 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "label", args[1], "remove"))
132 }
133 if errors.Is(err, store.ErrNotFound) {
134 return c.fail(protocol.ExitNotFound, "no label %q in %s", args[1], repo.Path())
135 }
136 return c.fail(protocol.ExitFailure, "%v", err)
137 }
138 return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
139 fmt.Fprintf(w, "removed label %s\n", args[1])
140 })
141}