internal/control/orglabel.go

v1.26.0
gitbay/internal/control/orglabel.go history · blame · raw

239 lines · 8264 bytes

  1package control
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"strings"
  8
  9	"gitbay.org/gitbay/internal/protocol"
 10	"gitbay.org/gitbay/internal/store"
 11)
 12
 13func init() {
 14	register(Command{Path: []string{"org", "label", "set"},
 15		Summary: "create an org label every org repository sees, or set its colour; folds in same-named repo labels",
 16		Usage:   "org label set <org> <label> [--color rrggbb|'']", Run: runOrgLabelSet})
 17	register(Command{Path: []string{"org", "label", "list"},
 18		Summary: "list an org's labels with use across the repositories you can read",
 19		Usage:   "org label list <org>", ReadOnly: true, Run: runOrgLabelList})
 20	register(Command{Path: []string{"org", "label", "remove"},
 21		Summary: "remove an org label from the org and from every issue under it",
 22		Usage:   "org label remove <org> <label>", Run: runOrgLabelRemove})
 23	register(Command{Path: []string{"org", "milestone", "create"},
 24		Summary: "create an org milestone spanning every org repository; folds in same-titled repo milestones",
 25		Usage:   "org milestone create <org> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runOrgMilestoneCreate})
 26	register(Command{Path: []string{"org", "milestone", "list"},
 27		Summary: "list an org's milestones with progress across the repositories you can read",
 28		Usage:   "org milestone list <org> [--state open|closed|all]", ReadOnly: true, Run: runOrgMilestoneList})
 29	register(Command{Path: []string{"org", "milestone", "close"},
 30		Summary: "close an org milestone",
 31		Usage:   "org milestone close <org> <title>", Run: runOrgMilestoneClose})
 32	register(Command{Path: []string{"org", "milestone", "reopen"},
 33		Summary: "reopen an org milestone",
 34		Usage:   "org milestone reopen <org> <title>", Run: runOrgMilestoneReopen})
 35}
 36
 37// orgReader resolves an org for a read of its labels or milestones.
 38// Members read; an outsider reads when some repository under the org is
 39// readable, and is refused rather than told the org is missing otherwise,
 40// since an org's existence is public anyway. The readable ids come back
 41// because every read counts over them.
 42func orgReader(c *Ctx, name string) (store.Org, []int64, int) {
 43	org, err := c.Store.OrgByName(name)
 44	if errors.Is(err, store.ErrNotFound) {
 45		return org, nil, c.fail(protocol.ExitNotFound, "no organization %q", name)
 46	}
 47	if err != nil {
 48		return org, nil, c.fail(protocol.ExitFailure, "%v", err)
 49	}
 50	readable, err := ReadableOrgRepoIDs(c.Store, c.User, org.ID)
 51	if err != nil {
 52		return org, nil, c.fail(protocol.ExitFailure, "%v", err)
 53	}
 54	role, err := c.Store.OrgRole(org.ID, c.User.ID)
 55	if err != nil {
 56		return org, nil, c.fail(protocol.ExitFailure, "%v", err)
 57	}
 58	if role == "" && len(readable) == 0 {
 59		return org, nil, c.fail(protocol.ExitDenied, "labels and milestones of %s are visible to its members; ask an admin to add you", name)
 60	}
 61	return org, readable, -1
 62}
 63
 64func runOrgLabelSet(c *Ctx, args []string) int {
 65	f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: 2, Usage: c.Cmd.Usage})
 66	if err != nil {
 67		return c.fail(protocol.ExitUsage, "%v", err)
 68	}
 69	orgName, name := f.pos(0), f.pos(1)
 70	color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color")
 71	if orgName == "" || name == "" {
 72		return c.usage()
 73	}
 74	if name == "" || len(name) > 50 {
 75		return c.fail(protocol.ExitUsage, "a label is 1 to 50 characters")
 76	}
 77	if colorSet && color != "" {
 78		if !labelColorPat.MatchString(color) {
 79			return c.fail(protocol.ExitUsage, "--color takes rrggbb (with or without #), or '' to clear")
 80		}
 81		color = "#" + strings.TrimPrefix(color, "#")
 82	}
 83	org, code := orgAdmin(c, orgName)
 84	if code >= 0 {
 85		return code
 86	}
 87	if !colorSet {
 88		// Keep the colour it has, if any; this is "make sure it exists".
 89		if labels, err := c.Store.ListOrgLabels(org.ID, nil); err == nil {
 90			for _, l := range labels {
 91				if l.Name == name {
 92					color = l.Color
 93				}
 94			}
 95		}
 96	}
 97	folded, err := c.Store.SetOrgLabel(org.ID, name, color)
 98	if err != nil {
 99		return c.fail(protocol.ExitFailure, "%v", err)
100	}
101	return c.emit(struct {
102		Name   string `json:"name"`
103		Color  string `json:"color,omitempty"`
104		Folded int    `json:"folded"`
105	}{name, color, folded}, func(w io.Writer) {
106		if color == "" {
107			fmt.Fprintf(w, "org label %s on %s, no colour set", name, org.Name)
108		} else {
109			fmt.Fprintf(w, "org label %s on %s is %s", name, org.Name, color)
110		}
111		if folded > 0 {
112			fmt.Fprintf(w, "; folded in %d repositor%s", folded, map[bool]string{true: "y", false: "ies"}[folded == 1])
113		}
114		fmt.Fprintln(w)
115	})
116}
117
118func runOrgLabelList(c *Ctx, args []string) int {
119	if len(args) != 1 {
120		return c.usage()
121	}
122	org, readable, code := orgReader(c, args[0])
123	if code >= 0 {
124		return code
125	}
126	labels, err := c.Store.ListOrgLabels(org.ID, readable)
127	if err != nil {
128		return c.fail(protocol.ExitFailure, "%v", err)
129	}
130	return c.emit(labels, func(w io.Writer) {
131		for _, l := range labels {
132			fmt.Fprintf(w, "%s\t%s\t%d\n", l.Name, l.Color, l.Issues)
133		}
134	})
135}
136
137func runOrgLabelRemove(c *Ctx, args []string) int {
138	if len(args) != 2 {
139		return c.usage()
140	}
141	org, code := orgAdmin(c, args[0])
142	if code >= 0 {
143		return code
144	}
145	if err := c.Store.DeleteOrgLabel(org.ID, args[1]); err != nil {
146		if errors.Is(err, store.ErrNotFound) {
147			return c.fail(protocol.ExitNotFound, "no org label %q on %s", args[1], org.Name)
148		}
149		return c.fail(protocol.ExitFailure, "%v", err)
150	}
151	return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
152		fmt.Fprintf(w, "removed org label %s from %s\n", args[1], org.Name)
153	})
154}
155
156func runOrgMilestoneCreate(c *Ctx, args []string) int {
157	f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--due"}, MaxPos: 2, Usage: c.Cmd.Usage})
158	if err != nil {
159		return c.fail(protocol.ExitUsage, "%v", err)
160	}
161	orgName, title, description, due := f.pos(0), f.pos(1), f.Value("--description"), f.Value("--due")
162	if orgName == "" || title == "" {
163		return c.usage()
164	}
165	if due != "" && !duePat.MatchString(due) {
166		return c.fail(protocol.ExitUsage, "--due must be YYYY-MM-DD")
167	}
168	org, code := orgAdmin(c, orgName)
169	if code >= 0 {
170		return code
171	}
172	_, folded, err := c.Store.CreateOrgMilestone(org.ID, title, description, due)
173	if err != nil {
174		return c.fail(protocol.ExitFailure, "%v", err)
175	}
176	return c.emit(struct {
177		Milestone string `json:"milestone"`
178		Folded    int    `json:"folded"`
179	}{title, folded}, func(w io.Writer) {
180		fmt.Fprintf(w, "created org milestone %q on %s", title, org.Name)
181		if folded > 0 {
182			fmt.Fprintf(w, "; folded in %d repositor%s", folded, map[bool]string{true: "y", false: "ies"}[folded == 1])
183		}
184		fmt.Fprintln(w)
185	})
186}
187
188func runOrgMilestoneList(c *Ctx, args []string) int {
189	f, err := parseFlags(args, flagSpec{Values: []string{"--state"}, MaxPos: 1, Usage: "org milestone list <org> [--state open|closed|all]"})
190	if err != nil {
191		return c.fail(protocol.ExitUsage, "%v", err)
192	}
193	state, orgName := "open", f.pos(0)
194	if f.Has("--state") {
195		state = f.Value("--state")
196	}
197	if orgName == "" || (state != "open" && state != "closed" && state != "all") {
198		return c.usage()
199	}
200	org, readable, code := orgReader(c, orgName)
201	if code >= 0 {
202		return code
203	}
204	ms, err := c.Store.ListOrgMilestones(org.ID, state, readable)
205	if err != nil {
206		return c.fail(protocol.ExitFailure, "%v", err)
207	}
208	return emitMilestones(c, ms)
209}
210
211func runOrgMilestoneClose(c *Ctx, args []string) int  { return setOrgMilestoneState(c, args, "closed") }
212func runOrgMilestoneReopen(c *Ctx, args []string) int { return setOrgMilestoneState(c, args, "open") }
213
214func setOrgMilestoneState(c *Ctx, args []string, state string) int {
215	verb := "close"
216	if state == "open" {
217		verb = "reopen"
218	}
219	if len(args) != 2 {
220		return c.usage()
221	}
222	org, code := orgAdmin(c, args[0])
223	if code >= 0 {
224		return code
225	}
226	m, err := c.Store.OrgMilestoneByTitle(org.ID, args[1])
227	if errors.Is(err, store.ErrNotFound) {
228		return c.fail(protocol.ExitNotFound, "no org milestone %q on %s", args[1], org.Name)
229	}
230	if err != nil {
231		return c.fail(protocol.ExitFailure, "%v", err)
232	}
233	if err := c.Store.SetMilestoneState(m.ID, state); err != nil {
234		return c.fail(protocol.ExitFailure, "%v", err)
235	}
236	return c.emit(map[string]string{"milestone": m.Title, "state": state}, func(w io.Writer) {
237		fmt.Fprintf(w, "%sd org milestone %q on %s\n", verb, m.Title, org.Name)
238	})
239}