| @@ -0,0 +1,143 @@ |
| 1 | package control |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "strings" |
| 8 | |
| 9 | "gitbay.org/gitbay/internal/protocol" |
| 10 | "gitbay.org/gitbay/internal/store" |
| 11 | ) |
| 12 | |
| 13 | func 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 | } |
| 24 | |
| 25 | // orgReader resolves an org for a read of its labels or milestones. |
| 26 | // Members read; an outsider reads when some repository under the org is |
| 27 | // readable, and is refused rather than told the org is missing otherwise, |
| 28 | // since an org's existence is public anyway. The readable ids come back |
| 29 | // because every read counts over them. |
| 30 | func orgReader(c *Ctx, name string) (store.Org, []int64, int) { |
| 31 | org, err := c.Store.OrgByName(name) |
| 32 | if errors.Is(err, store.ErrNotFound) { |
| 33 | return org, nil, c.fail(protocol.ExitNotFound, "no organization %q", name) |
| 34 | } |
| 35 | if err != nil { |
| 36 | return org, nil, c.fail(protocol.ExitFailure, "%v", err) |
| 37 | } |
| 38 | readable, err := ReadableOrgRepoIDs(c.Store, c.User, org.ID) |
| 39 | if err != nil { |
| 40 | return org, nil, c.fail(protocol.ExitFailure, "%v", err) |
| 41 | } |
| 42 | role, err := c.Store.OrgRole(org.ID, c.User.ID) |
| 43 | if err != nil { |
| 44 | return org, nil, c.fail(protocol.ExitFailure, "%v", err) |
| 45 | } |
| 46 | if role == "" && len(readable) == 0 { |
| 47 | return org, nil, c.fail(protocol.ExitDenied, "labels and milestones of %s are visible to its members", name) |
| 48 | } |
| 49 | return org, readable, -1 |
| 50 | } |
| 51 | |
| 52 | func runOrgLabelSet(c *Ctx, args []string) int { |
| 53 | const usage = "usage: org label set <org> <label> [--color rrggbb|'']" |
| 54 | f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: 2, Usage: usage}) |
| 55 | if err != nil { |
| 56 | return c.fail(protocol.ExitUsage, "%v", err) |
| 57 | } |
| 58 | orgName, name := f.pos(0), f.pos(1) |
| 59 | color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color") |
| 60 | if orgName == "" || name == "" { |
| 61 | return c.fail(protocol.ExitUsage, usage) |
| 62 | } |
| 63 | if name == "" || len(name) > 50 { |
| 64 | return c.fail(protocol.ExitUsage, "a label is 1 to 50 characters") |
| 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 | org, code := orgAdmin(c, orgName) |
| 73 | if code >= 0 { |
| 74 | return code |
| 75 | } |
| 76 | if !colorSet { |
| 77 | // Keep the colour it has, if any; this is "make sure it exists". |
| 78 | if labels, err := c.Store.ListOrgLabels(org.ID, nil); err == nil { |
| 79 | for _, l := range labels { |
| 80 | if l.Name == name { |
| 81 | color = l.Color |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | folded, err := c.Store.SetOrgLabel(org.ID, name, color) |
| 87 | if err != nil { |
| 88 | return c.fail(protocol.ExitFailure, "%v", err) |
| 89 | } |
| 90 | return c.emit(struct { |
| 91 | Name string `json:"name"` |
| 92 | Color string `json:"color,omitempty"` |
| 93 | Folded int `json:"folded"` |
| 94 | }{name, color, folded}, func(w io.Writer) { |
| 95 | if color == "" { |
| 96 | fmt.Fprintf(w, "org label %s on %s, no colour set", name, org.Name) |
| 97 | } else { |
| 98 | fmt.Fprintf(w, "org label %s on %s is %s", name, org.Name, color) |
| 99 | } |
| 100 | if folded > 0 { |
| 101 | fmt.Fprintf(w, "; folded in %d repositor%s", folded, map[bool]string{true: "y", false: "ies"}[folded == 1]) |
| 102 | } |
| 103 | fmt.Fprintln(w) |
| 104 | }) |
| 105 | } |
| 106 | |
| 107 | func runOrgLabelList(c *Ctx, args []string) int { |
| 108 | if len(args) != 1 { |
| 109 | return c.fail(protocol.ExitUsage, "usage: org label list <org>") |
| 110 | } |
| 111 | org, readable, code := orgReader(c, args[0]) |
| 112 | if code >= 0 { |
| 113 | return code |
| 114 | } |
| 115 | labels, err := c.Store.ListOrgLabels(org.ID, readable) |
| 116 | if err != nil { |
| 117 | return c.fail(protocol.ExitFailure, "%v", err) |
| 118 | } |
| 119 | return c.emit(labels, func(w io.Writer) { |
| 120 | for _, l := range labels { |
| 121 | fmt.Fprintf(w, "%s\t%s\t%d\n", l.Name, l.Color, l.Issues) |
| 122 | } |
| 123 | }) |
| 124 | } |
| 125 | |
| 126 | func runOrgLabelRemove(c *Ctx, args []string) int { |
| 127 | if len(args) != 2 { |
| 128 | return c.fail(protocol.ExitUsage, "usage: org label remove <org> <label>") |
| 129 | } |
| 130 | org, code := orgAdmin(c, args[0]) |
| 131 | if code >= 0 { |
| 132 | return code |
| 133 | } |
| 134 | if err := c.Store.DeleteOrgLabel(org.ID, args[1]); err != nil { |
| 135 | if errors.Is(err, store.ErrNotFound) { |
| 136 | return c.fail(protocol.ExitNotFound, "no org label %q on %s", args[1], org.Name) |
| 137 | } |
| 138 | return c.fail(protocol.ExitFailure, "%v", err) |
| 139 | } |
| 140 | return c.emit(map[string]string{"removed": args[1]}, func(w io.Writer) { |
| 141 | fmt.Fprintf(w, "removed org label %s from %s\n", args[1], org.Name) |
| 142 | }) |
| 143 | } |