internal/httpd/control.go
151 lines · 4280 bytes
1package httpd
2
3import (
4 "bytes"
5 "encoding/json"
6 "strings"
7
8 "gitbay.org/gitbay/internal/control"
9 "gitbay.org/gitbay/internal/gitutil"
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// runControl executes a control command as the browser session's user,
15// through the same registry the CLI and the JSON API reach. Web writes
16// never reimplement command logic — merge gates, review rules, and audit
17// entries stay in one place — so the surfaces cannot drift apart.
18//
19// ViaAPI is set, which refuses SSHOnly commands: anything whose input is a
20// credential (secrets, mirror tokens, session minting) stays on SSH.
21func (s *Server) runControl(u store.User, argv []string) (out string, msg string, ok bool) {
22 var stdout, stderr bytes.Buffer
23 ctx := &control.Ctx{
24 User: u,
25 Source: "web",
26 Scope: "full",
27 Store: s.st,
28 Cfg: s.cfg,
29 Stdin: strings.NewReader(""),
30 Stdout: &stdout,
31 Stderr: &stderr,
32 ViaAPI: true,
33 }
34 code := control.Dispatch(ctx, argv)
35 m := strings.TrimSpace(stderr.String())
36 if m == "" {
37 m = strings.TrimSpace(stdout.String())
38 }
39 return stdout.String(), m, code == protocol.ExitOK
40}
41
42// runControlJSON runs a command in JSON mode and returns its data object.
43// In JSON mode a failure is an envelope carrying the message rather than
44// stderr text, so both paths are read from the same envelope.
45func (s *Server) runControlJSON(u store.User, argv []string) (data map[string]any, msg string, ok bool) {
46 var stdout, stderr bytes.Buffer
47 ctx := &control.Ctx{
48 User: u,
49 Source: "web",
50 Scope: "full",
51 Store: s.st,
52 Cfg: s.cfg,
53 Stdin: strings.NewReader(""),
54 Stdout: &stdout,
55 Stderr: &stderr,
56 JSON: true,
57 ViaAPI: true,
58 }
59 code := control.Dispatch(ctx, argv)
60 var env struct {
61 Data map[string]any `json:"data"`
62 Error string `json:"error"`
63 }
64 json.Unmarshal(stdout.Bytes(), &env)
65 if code != protocol.ExitOK {
66 m := env.Error
67 if m == "" {
68 m = strings.TrimSpace(stderr.String())
69 }
70 if m == "" {
71 m = "the command failed"
72 }
73 return nil, m, false
74 }
75 return env.Data, "", true
76}
77
78// authorNames maps commit author addresses to account names for one
79// request. A commit carries whatever name git was configured with; when
80// the address is a verified address here, the account's own name is the
81// truthful one to show, and it links somewhere.
82type authorNames struct {
83 st *store.Store
84 cache map[string]string
85}
86
87func (s *Server) authorNames() *authorNames {
88 return &authorNames{st: s.st, cache: map[string]string{}}
89}
90
91// name returns the account name for an address, or the commit's own
92// author name when no account has verified it.
93func (a *authorNames) name(email, fallback string) string {
94 if email == "" {
95 return fallback
96 }
97 if got, ok := a.cache[email]; ok {
98 if got == "" {
99 return fallback
100 }
101 return got
102 }
103 name, _ := a.st.UsernameByVerifiedEmail(email)
104 a.cache[email] = name
105 if name == "" {
106 return fallback
107 }
108 return name
109}
110
111// account returns the account name behind an address, if any, so callers
112// can link the displayed name to a profile.
113func (a *authorNames) account(email string) (string, bool) {
114 if email == "" {
115 return "", false
116 }
117 if got, ok := a.cache[email]; ok {
118 return got, got != ""
119 }
120 name, _ := a.st.UsernameByVerifiedEmail(email)
121 a.cache[email] = name
122 return name, name != ""
123}
124
125// namedCommit is a listing commit plus the account behind its author
126// address, when there is one, so the name can link to a profile.
127type namedCommit struct {
128 gitutil.EntryCommit
129 User string
130}
131
132// namedCommits rewrites listing authors to account names where the
133// address is verified here.
134func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]namedCommit {
135 names := s.authorNames()
136 out := make(map[string]namedCommit, len(m))
137 for k, c := range m {
138 user, _ := names.account(c.Email)
139 c.Author = names.name(c.Email, c.Author)
140 out[k] = namedCommit{EntryCommit: c, User: user}
141 }
142 return out
143}
144
145// namedTip does the same for the single commit above a tree listing.
146func (s *Server) namedTip(c gitutil.EntryCommit) namedCommit {
147 names := s.authorNames()
148 user, _ := names.account(c.Email)
149 c.Author = names.name(c.Email, c.Author)
150 return namedCommit{EntryCommit: c, User: user}
151}