internal/control/orglabel.go

d775997ead04022093817ab513287b5886065247
gitbay/internal/control/orglabel.go history · blame · raw

241 lines · 8693 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", name)
 60	}
 61	return org, readable, -1
 62}
 63
 64func runOrgLabelSet(c *Ctx, args []string) int {
 65	const usage = "usage: org label set <org> <label> [--color rrggbb|'']"
 66	f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: 2, Usage: usage})
 67	if err != nil {
 68		return c.fail(protocol.ExitUsage, "%v", err)
 69	}
 70	orgName, name := f.pos(0), f.pos(1)
 71	color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color")
 72	if orgName == "" || name == "" {
 73		return c.fail(protocol.ExitUsage, usage)
 74	}
 75	if name == "" || len(name) > 50 {
 76		return c.fail(protocol.ExitUsage, "a label is 1 to 50 characters")
 77	}
 78	if colorSet && color != "" {
 79		if !labelColorPat.MatchString(color) {
 80			return c.fail(protocol.ExitUsage, "--color takes rrggbb (with or without #), or '' to clear")
 81		}
 82		color = "#" + strings.TrimPrefix(color, "#")
 83	}
 84	org, code := orgAdmin(c, orgName)
 85	if code >= 0 {
 86		return code
 87	}
 88	if !colorSet {
 89		// Keep the colour it has, if any; this is "make sure it exists".
 90		if labels, err := c.Store.ListOrgLabels(org.ID, nil); err == nil {
 91			for _, l := range labels {
 92				if l.Name == name {
 93					color = l.Color
 94				}
 95			}
 96		}
 97	}
 98	folded, err := c.Store.SetOrgLabel(org.ID, name, color)
 99	if err != nil {
100		return c.fail(protocol.ExitFailure, "%v", err)
101	}
102	return c.emit(struct {
103		Name   string `json:"name"`
104		Color  string `json:"color,omitempty"`
105		Folded int    `json:"folded"`
106	}{name, color, folded}, func(w io.Writer) {
107		if color == "" {
108			fmt.Fprintf(w, "org label %s on %s, no colour set", name, org.Name)
109		} else {
110			fmt.Fprintf(w, "org label %s on %s is %s", name, org.Name, color)
111		}
112		if folded > 0 {
113			fmt.Fprintf(w, "; folded in %d repositor%s", folded, map[bool]string{true: "y", false: "ies"}[folded == 1])
114		}
115		fmt.Fprintln(w)
116	})
117}
118
119func runOrgLabelList(c *Ctx, args []string) int {
120	if len(args) != 1 {
121		return c.fail(protocol.ExitUsage, "usage: org label list <org>")
122	}
123	org, readable, code := orgReader(c, args[0])
124	if code >= 0 {
125		return code
126	}
127	labels, err := c.Store.ListOrgLabels(org.ID, readable)
128	if err != nil {
129		return c.fail(protocol.ExitFailure, "%v", err)
130	}
131	return c.emit(labels, func(w io.Writer) {
132		for _, l := range labels {
133			fmt.Fprintf(w, "%s\t%s\t%d\n", l.Name, l.Color, l.Issues)
134		}
135	})
136}
137
138func runOrgLabelRemove(c *Ctx, args []string) int {
139	if len(args) != 2 {
140		return c.fail(protocol.ExitUsage, "usage: org label remove <org> <label>")
141	}
142	org, code := orgAdmin(c, args[0])
143	if code >= 0 {
144		return code
145	}
146	if err := c.Store.DeleteOrgLabel(org.ID, args[1]); err != nil {
147		if errors.Is(err, store.ErrNotFound) {
148			return c.fail(protocol.ExitNotFound, "no org label %q on %s", args[1], org.Name)
149		}
150		return c.fail(protocol.ExitFailure, "%v", err)
151	}
152	return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) {
153		fmt.Fprintf(w, "removed org label %s from %s\n", args[1], org.Name)
154	})
155}
156
157func runOrgMilestoneCreate(c *Ctx, args []string) int {
158	const usage = "usage: org milestone create <org> <title> [--description <d>] [--due YYYY-MM-DD]"
159	f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--due"}, MaxPos: 2, Usage: usage})
160	if err != nil {
161		return c.fail(protocol.ExitUsage, "%v", err)
162	}
163	orgName, title, description, due := f.pos(0), f.pos(1), f.Value("--description"), f.Value("--due")
164	if orgName == "" || title == "" {
165		return c.fail(protocol.ExitUsage, usage)
166	}
167	if due != "" && !duePat.MatchString(due) {
168		return c.fail(protocol.ExitUsage, "--due must be YYYY-MM-DD")
169	}
170	org, code := orgAdmin(c, orgName)
171	if code >= 0 {
172		return code
173	}
174	_, folded, err := c.Store.CreateOrgMilestone(org.ID, title, description, due)
175	if err != nil {
176		return c.fail(protocol.ExitFailure, "%v", err)
177	}
178	return c.emit(struct {
179		Milestone string `json:"milestone"`
180		Folded    int    `json:"folded"`
181	}{title, folded}, func(w io.Writer) {
182		fmt.Fprintf(w, "created org milestone %q on %s", title, org.Name)
183		if folded > 0 {
184			fmt.Fprintf(w, "; folded in %d repositor%s", folded, map[bool]string{true: "y", false: "ies"}[folded == 1])
185		}
186		fmt.Fprintln(w)
187	})
188}
189
190func runOrgMilestoneList(c *Ctx, args []string) int {
191	f, err := parseFlags(args, flagSpec{Values: []string{"--state"}, MaxPos: 1, Usage: "org milestone list <org> [--state open|closed|all]"})
192	if err != nil {
193		return c.fail(protocol.ExitUsage, "%v", err)
194	}
195	state, orgName := "open", f.pos(0)
196	if f.Has("--state") {
197		state = f.Value("--state")
198	}
199	if orgName == "" || (state != "open" && state != "closed" && state != "all") {
200		return c.fail(protocol.ExitUsage, "usage: org milestone list <org> [--state open|closed|all]")
201	}
202	org, readable, code := orgReader(c, orgName)
203	if code >= 0 {
204		return code
205	}
206	ms, err := c.Store.ListOrgMilestones(org.ID, state, readable)
207	if err != nil {
208		return c.fail(protocol.ExitFailure, "%v", err)
209	}
210	return emitMilestones(c, ms)
211}
212
213func runOrgMilestoneClose(c *Ctx, args []string) int  { return setOrgMilestoneState(c, args, "closed") }
214func runOrgMilestoneReopen(c *Ctx, args []string) int { return setOrgMilestoneState(c, args, "open") }
215
216func setOrgMilestoneState(c *Ctx, args []string, state string) int {
217	verb := "close"
218	if state == "open" {
219		verb = "reopen"
220	}
221	if len(args) != 2 {
222		return c.fail(protocol.ExitUsage, "usage: org milestone %s <org> <title>", verb)
223	}
224	org, code := orgAdmin(c, args[0])
225	if code >= 0 {
226		return code
227	}
228	m, err := c.Store.OrgMilestoneByTitle(org.ID, args[1])
229	if errors.Is(err, store.ErrNotFound) {
230		return c.fail(protocol.ExitNotFound, "no org milestone %q on %s", args[1], org.Name)
231	}
232	if err != nil {
233		return c.fail(protocol.ExitFailure, "%v", err)
234	}
235	if err := c.Store.SetMilestoneState(m.ID, state); err != nil {
236		return c.fail(protocol.ExitFailure, "%v", err)
237	}
238	return c.emit(map[string]string{"milestone": m.Title, "state": state}, func(w io.Writer) {
239		fmt.Fprintf(w, "%sd org milestone %q on %s\n", verb, m.Title, org.Name)
240	})
241}