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