A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 2f3a60f2e9

2f3a60f2e906a6e466d35f0dc26021512142a1cf

parent: 97bab07d1a

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-26T03:53:31Z

web: the login page names the host, not the display title

basePage carries both now: Site is the display name, Host is what a command
or URL must use. Setting [web] title made the login page print
"ssh git@GitBay web login".
e2e/accounts_test.go +21
@@ -217,3 +217,24 @@ func TestViewOnlyHasNoLoginOnTheWire(t *testing.T) {
217217 t.Fatalf("web login in view_only: exit %d, %s", code, errOut)
218218 }
219219 }
220
221// TestTitleIsNotAHostname pins the split between the instance's display name
222// and its hostname: the login page prints a command to paste into a terminal,
223// so it must name the host even when the operator has set a display title.
224func TestTitleIsNotAHostname(t *testing.T) {
225 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\ntitle = \"GitBay\"\n")
226
227 status, body := inst.get(t, "/login")
228 if status != 200 {
229 t.Fatalf("/login = %d", status)
230 }
231 if strings.Contains(body, "ssh git@GitBay") {
232 t.Fatal("login page tells you to ssh to the display title")
233 }
234 if !strings.Contains(body, "ssh git@gitbay.test web login") {
235 t.Fatalf("login page does not name the host:\n%s", body)
236 }
237 if !strings.Contains(body, "GitBay") {
238 t.Fatal("login page dropped the display title entirely")
239 }
240}
internal/httpd/accounts.go +4 −4
@@ -66,7 +66,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
6666 s.render(w, "login.html", struct {
6767 basePage
6868 Error string
69 }{basePage{Site: s.siteName()}, ""})
69 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, ""})
7070 return
7171 }
7272 userID, err := s.st.ConsumeLoginToken(store.HashToken(token))
@@ -74,7 +74,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
7474 s.render(w, "login.html", struct {
7575 basePage
7676 Error string
77 }{basePage{Site: s.siteName()},
77 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()},
7878 "that login link is invalid, expired, or already used — mint a new one"})
7979 return
8080 }
@@ -226,7 +226,7 @@ func (s *Server) renderSignup(w http.ResponseWriter, errMsg, username string) {
226226 Mode string // open | invite
227227 Error string
228228 Username string
229 }{basePage{Site: s.siteName()}, s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username})
229 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, s.cfg.SiteHost(), s.cfg.Registration.Mode, errMsg, username})
230230 }
231231
232232 func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
@@ -248,7 +248,7 @@ func (s *Server) signupSubmit(w http.ResponseWriter, r *http.Request) {
248248 Username string
249249 Message string
250250 Host string
251 }{basePage{Site: s.siteName()}, username, msg, s.cfg.SiteHost()})
251 }{basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}, username, msg, s.cfg.SiteHost()})
252252 }
253253
254254 // issueCreateForm renders the new-issue form, prefilled from the repo's
internal/httpd/page.go +5 −2
@@ -36,7 +36,10 @@ func (r rail) Empty() bool { return len(r.Pinned) == 0 && len(r.Reviews) == 0 }
3636 // basePage is what the layout needs on every page, repo or not. Page
3737 // structs embed it so the rail and the site name are always in scope.
3838 type basePage struct {
39 // Site is the instance's display name; Host is the name a command or
40 // URL must use. Anything copy-pasteable takes Host.
3941 Site string
42 Host string
4043 Viewer string
4144 Rail rail
4245 }
@@ -45,7 +48,7 @@ type basePage struct {
4548 // resolved a viewer.
4649 func (s *Server) base(r *http.Request) basePage {
4750 if s.cfg.Web.Mode != "accounts" {
48 return basePage{Site: s.siteName()}
51 return basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}
4952 }
5053 return s.baseFor(s.viewer(r))
5154 }
@@ -53,7 +56,7 @@ func (s *Server) base(r *http.Request) basePage {
5356 // baseFor is base for a handler that already holds the viewer, so the
5457 // session lookup is not repeated.
5558 func (s *Server) baseFor(viewer store.User) basePage {
56 b := basePage{Site: s.siteName()}
59 b := basePage{Site: s.siteName(), Host: s.cfg.SiteHost()}
5760 if viewer.ID == 0 {
5861 return b
5962 }
internal/web/templates/login.html +1 −1
@@ -4,6 +4,6 @@
44 {{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
55 <p>Browser sessions are minted over SSH — there is no password. From a machine
66 with your registered key:</p>
7<pre class="message">ssh git@{{.Site}} web login</pre>
7<pre class="message">ssh git@{{.Host}} web login</pre>
88 <p>then open the printed URL within five minutes.</p>
99 {{end}}