A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit 77812a5bbf

77812a5bbf967dd3948ea04859b0592d8d28ede9

parent: 30185df312

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24T18:52:19Z

web: collapse MR diffs by default

The diff sits in a <details> (no JS), collapsed, with a summary showing
files changed and +/- line counts computed from the classified diff.
Inline review threads expand with it; detached threads stay outside.
internal/httpd/web.go +58 −16
@@ -83,35 +83,61 @@ func (s *Server) describeAll(repos []store.Repo) []describedRepo {
8383 return out
8484 }
8585
86// index is the homepage: a dashboard for logged-in users, a landing page
87// for everyone else. The full public listing lives at /explore.
8688 func (s *Server) index(w http.ResponseWriter, r *http.Request) {
89 if s.cfg.Web.Mode == "accounts" {
90 if viewer := s.viewer(r); viewer.ID != 0 {
91 s.dashboard(w, r, viewer)
92 return
93 }
94 }
95 host := strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(
96 s.cfg.Server.SiteURL, "https://"), "http://"), "/")
97 s.render(w, "landing.html", struct {
98 Site string
99 Host string
100 Accounts bool
101 }{s.siteName(), host, s.cfg.Web.Mode == "accounts"})
102}
103
104func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, viewer store.User) {
105 pinned, _ := s.st.PinnedRepos(viewer.ID)
106 var visible []store.Repo
107 for _, rp := range pinned {
108 grant, _ := s.st.AccessRole(rp.ID, viewer.ID)
109 if policy.CanRead(viewer, rp, grant) {
110 visible = append(visible, rp)
111 }
112 }
113 mrs, _ := s.st.DashboardMRs(viewer.ID)
114 issues, _ := s.st.DashboardIssues(viewer.ID)
115 s.render(w, "dashboard.html", struct {
116 Site string
117 Viewer string
118 Pinned []describedRepo
119 MRs []store.DashboardItem
120 Issues []store.DashboardItem
121 }{s.siteName(), viewer.Username, s.describeAll(visible), mrs, issues})
122}
123
124func (s *Server) explore(w http.ResponseWriter, r *http.Request) {
87125 repos, err := s.st.ListPublicRepos()
88126 if err != nil {
89127 http.Error(w, "internal error", http.StatusInternalServerError)
90128 return
91129 }
92130 var viewer store.User
93 var mine []store.Repo
94131 if s.cfg.Web.Mode == "accounts" {
95 if viewer = s.viewer(r); viewer.ID != 0 {
96 all, err := s.st.ListReposForUser(viewer.ID)
97 if err == nil {
98 for _, rp := range all {
99 if rp.Visibility == "private" {
100 mine = append(mine, rp)
101 }
102 }
103 }
104 }
132 viewer = s.viewer(r)
105133 }
106134 q := strings.TrimSpace(r.URL.Query().Get("q"))
107 s.render(w, "index.html", struct {
135 s.render(w, "explore.html", struct {
108136 Site string
109137 Viewer string
110138 Query string
111139 Repos []describedRepo
112 Mine []describedRepo
113 }{s.siteName(), viewer.Username, q,
114 s.filterRepos(q, s.describeAll(repos)), s.filterRepos(q, s.describeAll(mine))})
140 }{s.siteName(), viewer.Username, q, s.filterRepos(q, s.describeAll(repos))})
115141 }
116142
117143 // filterRepos keeps repos whose path, description, or topics contain the
@@ -1041,6 +1067,21 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
10411067 md := s.ugcFor(r, p.Repo)
10421068 var detachedThreads []diffThread
10431069 lines, detachedThreads = attachThreads(lines, diffComments, m.HeadSHA, md)
1070 type diffStat struct{ Files, Adds, Dels int }
1071 var stat diffStat
1072 seenFiles := map[string]bool{}
1073 for _, l := range lines {
1074 switch l.Class {
1075 case "add":
1076 stat.Adds++
1077 case "del":
1078 stat.Dels++
1079 }
1080 if l.Path != "" && !seenFiles[l.Path] {
1081 seenFiles[l.Path] = true
1082 stat.Files++
1083 }
1084 }
10441085 s.render(w, "mr.html", struct {
10451086 repoPage
10461087 MR store.MR
@@ -1050,8 +1091,9 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
10501091 Comments []renderedComment
10511092 Reviews []store.MRReview
10521093 DiffLines []diffLine
1094 Stat diffStat
10531095 DetachedThreads []diffThread
1054 }{p, m, md(m.Body), checks, store.CombinedStatus(checks), renderComments(comments, md), reviews, lines, detachedThreads})
1096 }{p, m, md(m.Body), checks, store.CombinedStatus(checks), renderComments(comments, md), reviews, lines, stat, detachedThreads})
10551097 }
10561098
10571099 func (s *Server) refs(w http.ResponseWriter, r *http.Request) {
internal/web/static/style.css +24
@@ -337,6 +337,20 @@ a.memberchip {
337337 a.memberchip:hover { text-decoration: none; border-color: var(--accent); color: var(--accent); }
338338 a.memberchip .role { color: var(--muted); }
339339
340/* landing page */
341.landing { max-width: 44rem; padding: var(--sp-6) 0; }
342.landing h1 { font-size: 1.8rem; margin-bottom: var(--sp-3); }
343.landing .lede { font-size: var(--fs-4); color: var(--muted); line-height: 1.7; margin: 0 0 var(--sp-4); }
344pre.quickstart {
345 background: var(--code-bg);
346 border: 1px solid var(--faint);
347 border-radius: var(--r-md);
348 padding: var(--sp-3) var(--sp-4);
349 overflow-x: auto;
350 margin: 0 0 var(--sp-4);
351}
352.landing .explorelink { font-weight: 550; }
353
340354 /* index toolbar */
341355 p.toolbar { color: var(--muted); font-size: var(--fs-2); margin-top: var(--sp-4); }
342356 form.inline { display: inline; }
@@ -397,6 +411,16 @@ pre.message {
397411 }
398412
399413 /* diffs */
414details.difffold summary {
415 cursor: pointer;
416 display: flex;
417 align-items: baseline;
418 gap: var(--sp-3);
419 margin: var(--sp-5) 0 var(--sp-2);
420}
421details.difffold summary h3 { display: inline; margin: 0; }
422details.difffold summary .add { color: var(--ok); }
423details.difffold summary .del { color: var(--bad); }
400424 pre.diff {
401425 background: var(--code-bg);
402426 border: 1px solid var(--faint);
internal/web/templates/mr.html +3 −1
@@ -26,9 +26,11 @@
2626 <p><button type="submit">comment</button></p>
2727 </form>
2828 {{end}}
29<h3>diff</h3>
29<details class="difffold">
30<summary><h3>diff</h3><span class="meta">{{.Stat.Files}} file{{if ne .Stat.Files 1}}s{{end}} changed, <span class="add">+{{.Stat.Adds}}</span> <span class="del">−{{.Stat.Dels}}</span></span></summary>
3031 <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span>
3132 {{range .Threads}}</pre><div class="thread{{if .Resolved}} resolved{{end}}">{{if .Resolved}}<p class="threadstate">resolved by {{.Resolved}}</p>{{end}}{{range .Comments}}<p class="commenthead"><strong>{{.Author}}</strong> <span class="when">{{when .CreatedAt}}</span></p><div class="rendered">{{.BodyHTML}}</div>{{end}}</div><pre class="diff">{{end}}{{end}}</pre>
33</details>
3234 {{if .DetachedThreads}}<h3>threads on earlier revisions</h3>
3335 {{range .DetachedThreads}}<div class="thread stale"><p class="threadstate">{{if .Stale}}stale{{end}}{{if .Resolved}}{{if .Stale}} · {{end}}resolved by {{.Resolved}}{{end}}</p>{{range .Comments}}<p class="commenthead"><strong>{{.Author}}</strong> <span class="when">{{when .CreatedAt}}</span></p><div class="rendered">{{.BodyHTML}}</div>{{end}}</div>{{end}}{{end}}
3436 {{end}}