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}