internal/control/label.go

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

161 lines · 5042 bytes

6 symbols in this file
  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}