internal/control/orglabel.go
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}