krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: internal/control/org.go · raw
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
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{"org", "create"},
17 Summary: "create an organization (you become its first admin): org create <name>", Run: runOrgCreate})
18 register(Command{Path: []string{"org", "list"},
19 Summary: "list organizations you belong to", ReadOnly: true, Run: runOrgList})
20 register(Command{Path: []string{"org", "show"},
21 Summary: "show an organization and its members: org show <name>", ReadOnly: true, Run: runOrgShow})
22 register(Command{Path: []string{"org", "rename"},
23 Summary: "rename an organization: org rename <old> <new> (clone URLs change)", Run: runOrgRename})
24 register(Command{Path: []string{"org", "delete"},
25 Summary: "delete an empty organization: org delete <name> --yes", Run: runOrgDelete})
26 register(Command{Path: []string{"org", "members", "add"},
27 Summary: "add or update a member: org members add <org> <user> [--role member|admin]", Run: runOrgMembersAdd})
28 register(Command{Path: []string{"org", "members", "remove"},
29 Summary: "remove a member: org members remove <org> <user>", Run: runOrgMembersRemove})
30 register(Command{Path: []string{"org", "members", "list"},
31 Summary: "list members: org members list <org>", ReadOnly: true, Run: runOrgMembersList})
32}
33
34// orgAdmin loads an org and requires the caller to be one of its admins.
35func orgAdmin(c *Ctx, name string) (store.Org, int) {
36 org, err := c.Store.OrgByName(name)
37 if errors.Is(err, store.ErrNotFound) {
38 return org, c.fail(protocol.ExitNotFound, "no organization %q", name)
39 }
40 if err != nil {
41 return org, c.fail(protocol.ExitFailure, "%v", err)
42 }
43 role, err := c.Store.OrgRole(org.ID, c.User.ID)
44 if err != nil {
45 return org, c.fail(protocol.ExitFailure, "%v", err)
46 }
47 if role != "admin" {
48 return org, c.fail(protocol.ExitDenied, "only admins of %s can do that", name)
49 }
50 return org, -1
51}
52
53func runOrgCreate(c *Ctx, args []string) int {
54 if len(args) != 1 {
55 return c.fail(protocol.ExitUsage, "usage: org create <name>")
56 }
57 if err := policy.ValidateOwnerName(args[0]); err != nil {
58 return c.fail(protocol.ExitUsage, "%v", err)
59 }
60 if _, err := c.Store.CreateOrg(args[0], c.User.ID); err != nil {
61 return c.fail(protocol.ExitFailure, "%v", err)
62 }
63 return c.emit(map[string]string{"org": args[0], "role": "admin"}, func(w io.Writer) {
64 fmt.Fprintf(w, "created organization %s; you are its admin\n", args[0])
65 })
66}
67
68func runOrgList(c *Ctx, args []string) int {
69 orgs, err := c.Store.ListOrgsForUser(c.User.ID)
70 if err != nil {
71 return c.fail(protocol.ExitFailure, "%v", err)
72 }
73 type out struct {
74 Org string `json:"org"`
75 Role string `json:"role"`
76 }
77 var ds []out
78 for _, o := range orgs {
79 ds = append(ds, out{o.Username, o.Role})
80 }
81 return c.emit(ds, func(w io.Writer) {
82 for _, d := range ds {
83 fmt.Fprintf(w, "%s\t%s\n", d.Org, d.Role)
84 }
85 })
86}
87
88func runOrgShow(c *Ctx, args []string) int {
89 if len(args) != 1 {
90 return c.fail(protocol.ExitUsage, "usage: org show <name>")
91 }
92 org, err := c.Store.OrgByName(args[0])
93 if errors.Is(err, store.ErrNotFound) {
94 return c.fail(protocol.ExitNotFound, "no organization %q", args[0])
95 }
96 if err != nil {
97 return c.fail(protocol.ExitFailure, "%v", err)
98 }
99 members, err := c.Store.OrgMembers(org.ID)
100 if err != nil {
101 return c.fail(protocol.ExitFailure, "%v", err)
102 }
103 type memberOut struct {
104 User string `json:"user"`
105 Role string `json:"role"`
106 }
107 var ms []memberOut
108 for _, m := range members {
109 ms = append(ms, memberOut{m.Username, m.Role})
110 }
111 d := struct {
112 Org string `json:"org"`
113 Members []memberOut `json:"members"`
114 }{org.Name, ms}
115 return c.emit(d, func(w io.Writer) {
116 fmt.Fprintf(w, "%s\n", d.Org)
117 for _, m := range ms {
118 fmt.Fprintf(w, " %s\t%s\n", m.User, m.Role)
119 }
120 })
121}
122
123func runOrgRename(c *Ctx, args []string) int {
124 if len(args) != 2 {
125 return c.fail(protocol.ExitUsage, "usage: org rename <old> <new>")
126 }
127 org, code := orgAdmin(c, args[0])
128 if code >= 0 {
129 return code
130 }
131 newName := args[1]
132 if err := policy.ValidateOwnerName(newName); err != nil {
133 return c.fail(protocol.ExitUsage, "%v", err)
134 }
135 oldDir := filepath.Join(c.Cfg.Server.Root, "repos", org.Name)
136 newDir := filepath.Join(c.Cfg.Server.Root, "repos", newName)
137 if _, err := os.Stat(newDir); err == nil {
138 return c.fail(protocol.ExitFailure, "repository directory %s already exists", newName)
139 }
140 if err := c.Store.RenameOrg(org.ID, newName); err != nil {
141 return c.fail(protocol.ExitUsage, "%v", err)
142 }
143 // Repo paths on disk derive from the owner name; move the tree. If the
144 // move fails, revert the database so name and disk stay consistent.
145 if _, err := os.Stat(oldDir); err == nil {
146 if err := os.Rename(oldDir, newDir); err != nil {
147 c.Store.RenameOrg(org.ID, org.Name)
148 return c.fail(protocol.ExitFailure, "moving repositories: %v", err)
149 }
150 }
151 return c.emit(map[string]string{"org": newName, "was": org.Name}, func(w io.Writer) {
152 fmt.Fprintf(w, "renamed %s to %s — clone URLs now use %s/<repo>\n", org.Name, newName, newName)
153 })
154}
155
156func runOrgDelete(c *Ctx, args []string) int {
157 var name string
158 yes := false
159 for _, a := range args {
160 if a == "--yes" {
161 yes = true
162 } else if name == "" {
163 name = a
164 } else {
165 return c.fail(protocol.ExitUsage, "usage: org delete <name> --yes")
166 }
167 }
168 if name == "" {
169 return c.fail(protocol.ExitUsage, "usage: org delete <name> --yes")
170 }
171 org, code := orgAdmin(c, name)
172 if code >= 0 {
173 return code
174 }
175 if !yes {
176 return c.fail(protocol.ExitUsage, "org delete is permanent; re-run with --yes")
177 }
178 if err := c.Store.DeleteOrg(org.ID); err != nil {
179 return c.fail(protocol.ExitFailure, "%v", err)
180 }
181 return c.emit(map[string]string{"deleted": name}, func(w io.Writer) {
182 fmt.Fprintf(w, "deleted organization %s\n", name)
183 })
184}
185
186func runOrgMembersAdd(c *Ctx, args []string) int {
187 role := "member"
188 var rest []string
189 for i := 0; i < len(args); i++ {
190 if args[i] == "--role" {
191 if i+1 >= len(args) {
192 return c.fail(protocol.ExitUsage, "--role requires member|admin")
193 }
194 role = args[i+1]
195 i++
196 continue
197 }
198 rest = append(rest, args[i])
199 }
200 if len(rest) != 2 || (role != "member" && role != "admin") {
201 return c.fail(protocol.ExitUsage, "usage: org members add <org> <user> [--role member|admin]")
202 }
203 org, code := orgAdmin(c, rest[0])
204 if code >= 0 {
205 return code
206 }
207 target, err := c.Store.UserByUsername(rest[1])
208 if errors.Is(err, store.ErrNotFound) {
209 return c.fail(protocol.ExitNotFound, "no such user %q", rest[1])
210 }
211 if err != nil {
212 return c.fail(protocol.ExitFailure, "%v", err)
213 }
214 if err := c.Store.SetOrgMember(org.ID, target.ID, role); err != nil {
215 return c.fail(protocol.ExitUsage, "%v", err)
216 }
217 return c.emit(map[string]string{"org": org.Name, "user": target.Username, "role": role}, func(w io.Writer) {
218 fmt.Fprintf(w, "%s is now a %s of %s\n", target.Username, role, org.Name)
219 })
220}
221
222func runOrgMembersRemove(c *Ctx, args []string) int {
223 if len(args) != 2 {
224 return c.fail(protocol.ExitUsage, "usage: org members remove <org> <user>")
225 }
226 org, code := orgAdmin(c, args[0])
227 if code >= 0 {
228 return code
229 }
230 target, err := c.Store.UserByUsername(args[1])
231 if errors.Is(err, store.ErrNotFound) {
232 return c.fail(protocol.ExitNotFound, "no such user %q", args[1])
233 }
234 if err != nil {
235 return c.fail(protocol.ExitFailure, "%v", err)
236 }
237 if err := c.Store.RemoveOrgMember(org.ID, target.ID); err != nil {
238 if errors.Is(err, store.ErrNotFound) {
239 return c.fail(protocol.ExitNotFound, "%s is not a member of %s", target.Username, org.Name)
240 }
241 return c.fail(protocol.ExitUsage, "%v", err)
242 }
243 return c.emit(map[string]string{"org": org.Name, "removed": target.Username}, func(w io.Writer) {
244 fmt.Fprintf(w, "removed %s from %s\n", target.Username, org.Name)
245 })
246}
247
248func runOrgMembersList(c *Ctx, args []string) int {
249 if len(args) != 1 {
250 return c.fail(protocol.ExitUsage, "usage: org members list <org>")
251 }
252 return runOrgShow(c, args)
253}