internal/control/label.go

f8b976a97290a20d552056a999511f5d27d8e8ec
gitbay/internal/control/label.go history · blame · raw

141 lines · 4289 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 := c.parseArgs(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}