internal/control/label.go
161 lines · 5042 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
41// labelsScreen is a label list at a terminal: each label's name, colour
42// and how much carries it.
43func labelsScreen(labels []store.Label, actions ...action) screen {
44 rows := make([]row, len(labels))
45 for i, l := range labels {
46 org := ""
47 if l.Org {
48 org = "org"
49 }
50 rows[i] = rowOf(cRef(l.Name), cSwatch(l.Color), cMeta(fmt.Sprintf("%d issues", l.Issues), fmt.Sprintf("%d MRs", l.MRs), org))
51 }
52 return listScreen("Labels", rows, actions...)
53}
54
55func runLabelList(c *Ctx, args []string) int {
56 if len(args) != 1 {
57 return c.usage()
58 }
59 repo, code := resolveRepo(c, args[0], policy.CanRead)
60 if code >= 0 {
61 return code
62 }
63 readable, err := ReadableScope(c.Store, c.User, repo)
64 if err != nil {
65 return c.fail(protocol.ExitFailure, "%v", err)
66 }
67 labels, err := c.Store.ListLabels(repo, readable)
68 if err != nil {
69 return c.fail(protocol.ExitFailure, "%v", err)
70 }
71 return c.emitView(labels, func(w io.Writer) {
72 tb := c.table(w, "NAME", "COLOR", "ISSUES", "MRS")
73 for _, l := range labels {
74 cells := []cell{cRef(l.Name), cSwatch(l.Color), cNum(l.Issues), cNum(l.MRs)}
75 if l.Org {
76 cells = append(cells, cText("org"))
77 }
78 tb.row(cells...)
79 }
80 tb.flush()
81 }, func() screen {
82 s := labelsScreen(labels, action{"Labels", []string{"label", "set", repo.Path(), "<label>", "--color", "rrggbb"}})
83 if len(labels) > 0 {
84 s.actions = append(s.actions, action{"Read", []string{"issue", "list", repo.Path(), "--label", labels[0].Name}})
85 }
86 return s
87 })
88}
89
90func runLabelSet(c *Ctx, args []string) int {
91 f, err := c.parseArgs(args, flagSpec{Values: []string{"--color"}, MaxPos: -1, Usage: c.Cmd.Usage})
92 if err != nil {
93 return c.fail(protocol.ExitUsage, "%v", err)
94 }
95 rest := f.Pos
96 color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color")
97 if len(rest) != 2 {
98 return c.usage()
99 }
100 if colorSet && color != "" {
101 if !labelColorPat.MatchString(color) {
102 return c.fail(protocol.ExitUsage, "--color takes rrggbb (with or without #), or '' to clear")
103 }
104 color = "#" + strings.TrimPrefix(color, "#")
105 }
106 repo, code := resolveRepo(c, rest[0], policy.CanWrite)
107 if code >= 0 {
108 return code
109 }
110 if code := refuseArchived(c, repo); code >= 0 {
111 return code
112 }
113 name := rest[1]
114 if name == "" || len(name) > 50 {
115 return c.fail(protocol.ExitUsage, "a label is 1 to 50 characters")
116 }
117 if !colorSet {
118 // Keep the colour it has, if any; this is "make sure it exists".
119 if l, err := c.Store.LabelByName(repo, name); err == nil && !l.Org {
120 color = l.Color
121 }
122 }
123 if err := c.Store.SetLabel(repo, name, color); err != nil {
124 if errors.Is(err, store.ErrOrgScoped) {
125 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "label", name, "set"))
126 }
127 return c.fail(protocol.ExitFailure, "%v", err)
128 }
129 return c.emit(store.Label{Name: name, Color: color}, func(w io.Writer) {
130 if color == "" {
131 fmt.Fprintf(w, "label %s, no colour set\n", name)
132 return
133 }
134 fmt.Fprintf(w, "label %s is %s\n", name, color)
135 })
136}
137
138func runLabelRemove(c *Ctx, args []string) int {
139 if len(args) != 2 {
140 return c.usage()
141 }
142 repo, code := resolveRepo(c, args[0], policy.CanWrite)
143 if code >= 0 {
144 return code
145 }
146 if code := refuseArchived(c, repo); code >= 0 {
147 return code
148 }
149 if err := c.Store.DeleteLabel(repo, args[1]); err != nil {
150 if errors.Is(err, store.ErrOrgScoped) {
151 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "label", args[1], "remove"))
152 }
153 if errors.Is(err, store.ErrNotFound) {
154 return c.fail(protocol.ExitNotFound, "no label %q in %s", args[1], repo.Path())
155 }
156 return c.fail(protocol.ExitFailure, "%v", err)
157 }
158 return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
159 fmt.Fprintf(w, "removed label %s\n", args[1])
160 })
161}