krz/gitbay

A CLI-first git forge.

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

main: internal/control/profile.go · raw

  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{"profile", "show"},
 15		Summary: "show a user's or org's profile: profile show [name]", ReadOnly: true, Run: runProfileShow})
 16	register(Command{Path: []string{"profile", "set"},
 17		Summary: "set your profile: profile set [--description <d>] [--website <url>] ('' clears)", Run: runProfileSet})
 18	register(Command{Path: []string{"org", "profile"},
 19		Summary: "show or set an org's profile: org profile <org> [--description <d>] [--website <url>]", Run: runOrgProfile})
 20}
 21
 22// parseProfileFlags pulls --description/--website out of args; a flag given
 23// with an empty value clears the field, an absent flag leaves it untouched.
 24func parseProfileFlags(args []string) (rest []string, desc, site *string, err error) {
 25	for i := 0; i < len(args); i++ {
 26		switch args[i] {
 27		case "--description", "--website":
 28			if i+1 >= len(args) {
 29				return nil, nil, nil, fmt.Errorf("%s requires a value", args[i])
 30			}
 31			v := args[i+1]
 32			if args[i] == "--description" {
 33				desc = &v
 34			} else {
 35				site = &v
 36			}
 37			i++
 38		default:
 39			rest = append(rest, args[i])
 40		}
 41	}
 42	return rest, desc, site, nil
 43}
 44
 45func validateWebsite(url string) error {
 46	if url == "" || strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") {
 47		return nil
 48	}
 49	return errors.New("website must start with https:// or http://")
 50}
 51
 52func applyProfile(p store.Profile, desc, site *string) (store.Profile, error) {
 53	if desc != nil {
 54		d, _, _ := strings.Cut(strings.TrimSpace(*desc), "\n")
 55		if len(d) > 256 {
 56			d = d[:256]
 57		}
 58		p.Description = d
 59	}
 60	if site != nil {
 61		s := strings.TrimSpace(*site)
 62		if err := validateWebsite(s); err != nil {
 63			return p, err
 64		}
 65		p.Website = s
 66	}
 67	return p, nil
 68}
 69
 70type profileOut struct {
 71	Name        string `json:"name"`
 72	Kind        string `json:"kind"`
 73	Description string `json:"description,omitempty"`
 74	Website     string `json:"website,omitempty"`
 75}
 76
 77func emitProfile(c *Ctx, d profileOut) int {
 78	return c.emit(d, func(w io.Writer) {
 79		fmt.Fprintf(w, "%s (%s)\n", d.Name, d.Kind)
 80		if d.Description != "" {
 81			fmt.Fprintf(w, "%s\n", d.Description)
 82		}
 83		if d.Website != "" {
 84			fmt.Fprintf(w, "%s\n", d.Website)
 85		}
 86	})
 87}
 88
 89func runProfileShow(c *Ctx, args []string) int {
 90	name := c.User.Username
 91	if len(args) == 1 {
 92		name = args[0]
 93	} else if len(args) > 1 {
 94		return c.fail(protocol.ExitUsage, "usage: profile show [name]")
 95	}
 96	kind, id := "", int64(0)
 97	if u, err := c.Store.UserByUsername(name); err == nil {
 98		kind, id = "user", u.ID
 99	} else if o, err := c.Store.OrgByName(name); err == nil {
100		kind, id = "org", o.ID
101	} else {
102		return c.fail(protocol.ExitNotFound, "no user or organization %q", name)
103	}
104	p, err := c.Store.OwnerProfile(kind, id)
105	if err != nil {
106		return c.fail(protocol.ExitFailure, "%v", err)
107	}
108	return emitProfile(c, profileOut{name, kind, p.Description, p.Website})
109}
110
111func runProfileSet(c *Ctx, args []string) int {
112	rest, desc, site, err := parseProfileFlags(args)
113	if err != nil || len(rest) != 0 {
114		return c.fail(protocol.ExitUsage, "usage: profile set [--description <d>] [--website <url>]")
115	}
116	if desc == nil && site == nil {
117		return c.fail(protocol.ExitUsage, "nothing to set: pass --description and/or --website")
118	}
119	p, err := c.Store.OwnerProfile("user", c.User.ID)
120	if err != nil {
121		return c.fail(protocol.ExitFailure, "%v", err)
122	}
123	p, err = applyProfile(p, desc, site)
124	if err != nil {
125		return c.fail(protocol.ExitUsage, "%v", err)
126	}
127	if err := c.Store.SetOwnerProfile("user", c.User.ID, p); err != nil {
128		return c.fail(protocol.ExitFailure, "%v", err)
129	}
130	return emitProfile(c, profileOut{c.User.Username, "user", p.Description, p.Website})
131}
132
133func runOrgProfile(c *Ctx, args []string) int {
134	rest, desc, site, err := parseProfileFlags(args)
135	if err != nil || len(rest) != 1 {
136		return c.fail(protocol.ExitUsage, "usage: org profile <org> [--description <d>] [--website <url>]")
137	}
138	name := rest[0]
139	if desc == nil && site == nil {
140		return runProfileShow(c, []string{name})
141	}
142	org, code := orgAdmin(c, name)
143	if code >= 0 {
144		return code
145	}
146	p, err := c.Store.OwnerProfile("org", org.ID)
147	if err != nil {
148		return c.fail(protocol.ExitFailure, "%v", err)
149	}
150	p, err = applyProfile(p, desc, site)
151	if err != nil {
152		return c.fail(protocol.ExitUsage, "%v", err)
153	}
154	if err := c.Store.SetOwnerProfile("org", org.ID, p); err != nil {
155		return c.fail(protocol.ExitFailure, "%v", err)
156	}
157	return emitProfile(c, profileOut{org.Name, "org", p.Description, p.Website})
158}