krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

main: 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}