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}