A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 075af6c002

075af6c002f84e2edcbd793668a23239fbe18636

parent: d298215429

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24T17:03:23Z

web: designed 404 page

New notFound helper renders 404.html with a 404 status (plain-text
fallback if the template fails); web.go page handlers switch to it.
Private and missing resources stay indistinguishable. Empty states for
issues, MRs, logs, refs, and empty repos landed with their pages.
internal/httpd/web.go +27 −14
@@ -55,6 +55,19 @@ func (s *Server) favicon(w http.ResponseWriter, r *http.Request) {
5555 w.Write(web.FaviconSVG)
5656 }
5757
58// notFound renders the designed 404 page with a 404 status. Falls back to
59// the stock plain-text response if the template fails.
60func (s *Server) notFound(w http.ResponseWriter, r *http.Request) {
61 var buf bytes.Buffer
62 if err := web.Render(&buf, "404.html", struct{ Site string }{s.siteName()}); err != nil {
63 http.NotFound(w, r)
64 return
65 }
66 w.Header().Set("Content-Type", "text/html; charset=utf-8")
67 w.WriteHeader(http.StatusNotFound)
68 buf.WriteTo(w)
69}
70
5871 // describedRepo pairs a repo with its description for listings.
5972 type describedRepo struct {
6073 store.Repo
@@ -129,7 +142,7 @@ func (s *Server) repoFor(w http.ResponseWriter, r *http.Request, ref string) (re
129142 ok = policyCanRead(viewer, repo, grant)
130143 }
131144 if !ok {
132 http.NotFound(w, r)
145 s.notFound(w, r)
133146 return repoPage{}, false
134147 }
135148 if ref == "" {
@@ -186,7 +199,7 @@ func (s *Server) ownerPage(w http.ResponseWriter, r *http.Request) {
186199 kind, ownerID = "org", o.ID
187200 members, _ = s.st.OrgMembers(o.ID)
188201 } else {
189 http.NotFound(w, r)
202 s.notFound(w, r)
190203 return
191204 }
192205 profile, _ := s.st.OwnerProfile(kind, ownerID)
@@ -251,7 +264,7 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage,
251264 }
252265 entries, err := gitutil.ListTree(p.Dir, p.Ref, dirPath)
253266 if err != nil {
254 http.NotFound(w, r)
267 s.notFound(w, r)
255268 return
256269 }
257270 prefix := ""
@@ -286,7 +299,7 @@ func (s *Server) blob(w http.ResponseWriter, r *http.Request) {
286299 filePath := strings.Trim(r.PathValue("path"), "/")
287300 data, err := gitutil.ReadBlob(p.Dir, p.Ref, filePath, maxRenderBytes+1)
288301 if err != nil {
289 http.NotFound(w, r)
302 s.notFound(w, r)
290303 return
291304 }
292305 binary := gitutil.IsBinary(data) || len(data) > maxRenderBytes
@@ -338,7 +351,7 @@ func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
338351 filePath := strings.Trim(r.PathValue("path"), "/")
339352 data, err := gitutil.ReadBlob(p.Dir, p.Ref, filePath, s.cfg.Limits.MaxBlobBytes)
340353 if err != nil {
341 http.NotFound(w, r)
354 s.notFound(w, r)
342355 return
343356 }
344357 // Serve inert: never let repo content execute in the forge's origin.
@@ -577,7 +590,7 @@ func (s *Server) log(w http.ResponseWriter, r *http.Request) {
577590 const pageSize = 50
578591 shas, err := gitutil.RevList(p.Dir, p.Ref, pageSize+1)
579592 if err != nil {
580 http.NotFound(w, r)
593 s.notFound(w, r)
581594 return
582595 }
583596 next := ""
@@ -617,12 +630,12 @@ func (s *Server) commit(w http.ResponseWriter, r *http.Request) {
617630 sha := r.PathValue("sha")
618631 full, err := gitutil.ResolveRef(p.Dir, sha)
619632 if err != nil {
620 http.NotFound(w, r)
633 s.notFound(w, r)
621634 return
622635 }
623636 v, parsed := s.sigFor(p.Repo, p.Dir, full)
624637 if parsed == nil {
625 http.NotFound(w, r)
638 s.notFound(w, r)
626639 return
627640 }
628641 patch, _ := gitutil.ShowPatch(p.Dir, full, 4<<20)
@@ -708,12 +721,12 @@ func (s *Server) issue(w http.ResponseWriter, r *http.Request) {
708721 p.Tab = "issues"
709722 n, err := strconv.ParseInt(r.PathValue("n"), 10, 64)
710723 if err != nil {
711 http.NotFound(w, r)
724 s.notFound(w, r)
712725 return
713726 }
714727 iss, err := s.st.IssueByNumber(p.Repo.ID, n)
715728 if err != nil {
716 http.NotFound(w, r)
729 s.notFound(w, r)
717730 return
718731 }
719732 comments, err := s.st.ListIssueComments(iss.ID)
@@ -764,12 +777,12 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
764777 p.Tab = "merge requests"
765778 n, err := strconv.ParseInt(r.PathValue("n"), 10, 64)
766779 if err != nil {
767 http.NotFound(w, r)
780 s.notFound(w, r)
768781 return
769782 }
770783 m, err := s.st.MRByNumber(p.Repo.ID, n)
771784 if err != nil {
772 http.NotFound(w, r)
785 s.notFound(w, r)
773786 return
774787 }
775788 comments, _ := s.st.ListMRComments(m.ID)
@@ -827,11 +840,11 @@ func (s *Server) archive(w http.ResponseWriter, r *http.Request) {
827840 file := r.PathValue("file")
828841 ref, ok := strings.CutSuffix(file, ".tar.gz")
829842 if !ok {
830 http.NotFound(w, r)
843 s.notFound(w, r)
831844 return
832845 }
833846 if _, err := gitutil.ResolveRef(p.Dir, ref); err != nil {
834 http.NotFound(w, r)
847 s.notFound(w, r)
835848 return
836849 }
837850 prefix := fmt.Sprintf("%s-%s", p.Repo.Name, ref)
internal/web/static/style.css +12
@@ -463,6 +463,18 @@ button {
463463 }
464464 button:hover { filter: brightness(1.08); }
465465
466/* 404 */
467.notfound { text-align: center; padding: var(--sp-6) 0; }
468.notfound .bigcode {
469 font-family: var(--mono);
470 font-size: 4rem;
471 color: var(--faint);
472 -webkit-text-stroke: 1px var(--line);
473 margin: 0;
474 line-height: 1;
475}
476.notfound h1 { margin-top: var(--sp-3); }
477
466478 /* footer */
467479 footer {
468480 max-width: 64rem;
internal/web/templates/404.html added +10
@@ -0,0 +1,10 @@
1{{define "title"}}not found · {{.Site}}{{end}}
2{{define "content"}}
3<div class="notfound">
4 <p class="bigcode">404</p>
5 <h1>page not found</h1>
6 <p class="desc">nothing lives at this address — the path may be wrong,
7 or the repository may be private.</p>
8 <p><a href="/">← back to {{.Site}}</a></p>
9</div>
10{{end}}