Commit 9bf001e417
9bf001e41785e5c85d396154fe8d5cb2da235c14
parent: 51cfb502fd
Verified · cmc
cmc <hello@cleberg.net> · 2026-08-24T16:47:55Z
web: repo browsing pages
Tree: path bar with ref chip, breadcrumbs, and clone command; file
table with wide name column, right-aligned sizes, de-emphasized mode;
README card gets a filename header (ReadmeName passed from the
handler); empty repos show the push command. Log: list rows with
subject/author/date hierarchy, badge and short SHA on the right.
Commit: heading, full SHA, badges, author line grouped. Refs: icons,
short SHAs via a new template func. Blob: path bar with raw/edit
actions.
internal/httpd/web.go
+7 −4
| @@ -243,6 +243,7 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, |
| 243 | 243 | Crumbs []crumb |
| 244 | 244 | Prefix string |
| 245 | 245 | Entries []gitutil.TreeEntry |
| 246 | ReadmeName string |
| 246 | 247 | ReadmeHTML template.HTML |
| 247 | 248 | }{repoPage: p}) |
| 248 | 249 | return |
| @@ -258,9 +259,10 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, |
| 258 | 259 | } |
| 259 | 260 | |
| 260 | 261 | var readmeHTML template.HTML |
| 261 | | if name := pickReadme(entries); name != "" { |
| 262 | | if raw, err := gitutil.ReadBlob(p.Dir, p.Ref, prefix+name, maxRenderBytes); err == nil { |
| 263 | | readmeHTML = renderReadme(name, raw) |
| 262 | readmeName := pickReadme(entries) |
| 263 | if readmeName != "" { |
| 264 | if raw, err := gitutil.ReadBlob(p.Dir, p.Ref, prefix+readmeName, maxRenderBytes); err == nil { |
| 265 | readmeHTML = renderReadme(readmeName, raw) |
| 264 | 266 | } |
| 265 | 267 | } |
| 266 | 268 | |
| @@ -269,8 +271,9 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, |
| 269 | 271 | Crumbs []crumb |
| 270 | 272 | Prefix string |
| 271 | 273 | Entries []gitutil.TreeEntry |
| 274 | ReadmeName string |
| 272 | 275 | ReadmeHTML template.HTML |
| 273 | | }{p, crumbs(p, "tree", dirPath), prefix, entries, readmeHTML}) |
| 276 | }{p, crumbs(p, "tree", dirPath), prefix, entries, readmeName, readmeHTML}) |
| 274 | 277 | } |
| 275 | 278 | |
| 276 | 279 | func (s *Server) blob(w http.ResponseWriter, r *http.Request) { |
internal/web/static/style.css
+76 −1
| @@ -150,12 +150,87 @@ nav.tabs a { |
| 150 | 150 | nav.tabs a:hover { color: var(--fg); background: var(--surface); text-decoration: none; } |
| 151 | 151 | nav.tabs a.active { color: var(--fg); border-bottom-color: var(--accent); font-weight: 550; } |
| 152 | 152 | |
| 153 | | p.clone code { |
| 153 | .clone { color: var(--muted); font-size: var(--fs-2); } |
| 154 | .clone code { |
| 154 | 155 | background: var(--surface); |
| 155 | 156 | border: 1px solid var(--faint); |
| 156 | 157 | padding: 0.15rem 0.5rem; |
| 157 | 158 | border-radius: var(--r-sm); |
| 158 | 159 | } |
| 160 | |
| 161 | /* path bar on tree and blob pages */ |
| 162 | .pathbar { |
| 163 | display: flex; |
| 164 | flex-wrap: wrap; |
| 165 | align-items: center; |
| 166 | gap: var(--sp-2) var(--sp-3); |
| 167 | margin: var(--sp-3) 0; |
| 168 | font-size: var(--fs-2); |
| 169 | } |
| 170 | .pathbar .spacer { flex: 1; } |
| 171 | .refchip { |
| 172 | display: inline-flex; |
| 173 | align-items: center; |
| 174 | gap: 0.3rem; |
| 175 | background: var(--surface); |
| 176 | border: 1px solid var(--faint); |
| 177 | border-radius: var(--r-md); |
| 178 | padding: 0.1rem 0.55rem; |
| 179 | font-family: var(--mono); |
| 180 | font-size: var(--fs-1); |
| 181 | color: var(--fg); |
| 182 | } |
| 183 | .pathbar .crumbs strong { color: var(--fg); } |
| 184 | |
| 185 | /* file table */ |
| 186 | table.tree td.name { width: 100%; } |
| 187 | table.tree td.name a { color: var(--fg); } |
| 188 | table.tree td.name a:hover { color: var(--accent); } |
| 189 | table.tree td.size { text-align: right; } |
| 190 | table.tree td.empty, table.refs td.empty, li.empty, ul.loglist li.empty { color: var(--muted); } |
| 191 | |
| 192 | /* refs page */ |
| 193 | table.refs td.name { width: 100%; } |
| 194 | table.refs td.sha { color: var(--muted); } |
| 195 | |
| 196 | /* card header (README) */ |
| 197 | .cardhead { |
| 198 | font-family: var(--mono); |
| 199 | font-size: var(--fs-1); |
| 200 | color: var(--muted); |
| 201 | border-bottom: 1px solid var(--faint); |
| 202 | margin: calc(-1 * var(--sp-4)) calc(-1 * var(--sp-5)) var(--sp-4); |
| 203 | padding: var(--sp-2) var(--sp-5); |
| 204 | background: var(--surface); |
| 205 | border-radius: var(--r-lg) var(--r-lg) 0 0; |
| 206 | } |
| 207 | |
| 208 | /* log */ |
| 209 | ul.loglist { list-style: none; margin: var(--sp-3) 0; padding: 0; } |
| 210 | ul.loglist li { |
| 211 | display: flex; |
| 212 | align-items: baseline; |
| 213 | gap: var(--sp-3); |
| 214 | padding: var(--sp-2) 0; |
| 215 | border-bottom: 1px solid var(--faint); |
| 216 | } |
| 217 | ul.loglist .commitmain { flex: 1; min-width: 0; } |
| 218 | ul.loglist p { margin: 0; } |
| 219 | ul.loglist .subject a { color: var(--fg); font-weight: 550; } |
| 220 | ul.loglist .subject a:hover { color: var(--accent); } |
| 221 | ul.loglist .commitside { |
| 222 | display: flex; |
| 223 | align-items: baseline; |
| 224 | gap: var(--sp-2); |
| 225 | white-space: nowrap; |
| 226 | } |
| 227 | p.pager { margin-top: var(--sp-4); } |
| 228 | |
| 229 | /* commit page */ |
| 230 | .commithead p { margin: var(--sp-1) 0; } |
| 231 | .commithead h2 { margin-bottom: var(--sp-1); } |
| 232 | code.fullsha { color: var(--muted); overflow-wrap: anywhere; } |
| 233 | .empty-note { color: var(--muted); } |
| 159 | 234 | .crumbs { color: var(--muted); font-size: var(--fs-2); } |
| 160 | 235 | .desc, td.desc { color: var(--muted); } |
| 161 | 236 | .error { color: var(--bad); } |
internal/web/templates/blob.html
+7 −3
| @@ -1,8 +1,12 @@ |
| 1 | 1 | {{define "title"}}{{.Path}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | 2 | {{define "content"}} |
| 3 | 3 | {{template "repoheader" .}} |
| 4 | | <p class="crumbs">{{.Ref}}: {{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}{{.Base}} |
| 5 | | · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}">raw</a>{{if .Viewer}} · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/edit/{{.Ref}}/{{.Path}}">edit</a>{{end}}</p> |
| 6 | | {{if .Binary}}<p>binary file, {{.Size}} bytes — <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}">download</a></p> |
| 4 | <div class="pathbar"> |
| 5 | <span class="refchip">{{template "branchicon"}} {{.Ref}}</span> |
| 6 | <span class="crumbs"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>/{{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}<strong>{{.Base}}</strong></span> |
| 7 | <span class="spacer"></span> |
| 8 | <span class="actions"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}">raw</a>{{if .Viewer}} · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/edit/{{.Ref}}/{{.Path}}">edit</a>{{end}}</span> |
| 9 | </div> |
| 10 | {{if .Binary}}<p class="empty-note">binary file, {{.Size}} bytes — <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}">download</a></p> |
| 7 | 11 | {{else}}<div class="code">{{.CodeHTML}}</div>{{end}} |
| 8 | 12 | {{end}} |
internal/web/templates/commit.html
+7 −4
| @@ -1,10 +1,13 @@ |
| 1 | 1 | {{define "title"}}{{.ShortSHA}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | 2 | {{define "content"}} |
| 3 | 3 | {{template "repoheader" .}} |
| 4 | | <h2><code>{{.SHA}}</code></h2> |
| 5 | | <p>{{template "sigbadge" .Sig}}{{range .Checks}} <span class="badge check-{{.State}}">{{.Context}}: {{.State}}</span>{{end}}</p> |
| 6 | | <p>author: {{.AuthorName}} <{{.AuthorEmail}}> · {{.Date}} |
| 7 | | {{if .CommitterEmail}}<br>committer: <{{.CommitterEmail}}>{{end}}</p> |
| 4 | <div class="commithead"> |
| 5 | <h2>commit <code>{{.ShortSHA}}</code></h2> |
| 6 | <p class="meta"><code class="fullsha">{{.SHA}}</code></p> |
| 7 | <p>{{template "sigbadge" .Sig}}{{range .Checks}} <span class="badge check-{{.State}}">{{.Context}}: {{.State}}</span>{{end}}</p> |
| 8 | <p class="meta"><span title="{{.AuthorEmail}}">{{.AuthorName}}</span> <{{.AuthorEmail}}> · {{.Date}} |
| 9 | {{if .CommitterEmail}}<br>committer: <{{.CommitterEmail}}>{{end}}</p> |
| 10 | </div> |
| 8 | 11 | <pre class="message">{{.Message}}</pre> |
| 9 | 12 | <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span> |
| 10 | 13 | {{end}}</pre> |
internal/web/templates/log.html
+14 −11
| @@ -1,15 +1,18 @@ |
| 1 | 1 | {{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | 2 | {{define "content"}} |
| 3 | 3 | {{template "repoheader" .}} |
| 4 | | <table class="log"> |
| 5 | | {{range .Commits}}<tr> |
| 6 | | <td><code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code></td> |
| 7 | | <td>{{.Subject}}</td> |
| 8 | | <td>{{.AuthorName}} <{{.AuthorEmail}}></td> |
| 9 | | <td>{{.Date}}</td> |
| 10 | | <td>{{template "sigbadge" .Sig}}</td> |
| 11 | | </tr> |
| 12 | | {{end}} |
| 13 | | </table> |
| 14 | | {{if .NextSHA}}<p><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log/{{.NextSHA}}">older →</a></p>{{end}} |
| 4 | <ul class="loglist"> |
| 5 | {{range .Commits}}<li> |
| 6 | <div class="commitmain"> |
| 7 | <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p> |
| 8 | <p class="meta"><span title="{{.AuthorEmail}}">{{.AuthorName}}</span> · {{.Date}}</p> |
| 9 | </div> |
| 10 | <div class="commitside"> |
| 11 | {{template "sigbadge" .Sig}} |
| 12 | <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code> |
| 13 | </div> |
| 14 | </li> |
| 15 | {{else}}<li class="empty">no commits on {{.Ref}} yet</li>{{end}} |
| 16 | </ul> |
| 17 | {{if .NextSHA}}<p class="pager"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log/{{.NextSHA}}">older →</a></p>{{end}} |
| 15 | 18 | {{end}} |
internal/web/templates/refs.html
+2 −2
| @@ -2,7 +2,7 @@ |
| 2 | 2 | {{define "content"}} |
| 3 | 3 | {{template "repoheader" .}} |
| 4 | 4 | <h2>branches</h2> |
| 5 | | <table>{{range .Branches}}<tr><td><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{.Name}}/">{{.Name}}</a></td><td><code>{{.SHA}}</code></td></tr>{{end}}</table> |
| 5 | <table class="refs">{{range .Branches}}<tr><td class="name">{{template "branchicon"}} <a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{.Name}}/">{{.Name}}</a></td><td class="sha"><code>{{short .SHA}}</code></td></tr>{{else}}<tr><td class="empty">none</td></tr>{{end}}</table> |
| 6 | 6 | <h2>tags</h2> |
| 7 | | <table>{{range .Tags}}<tr><td><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{.Name}}/">{{.Name}}</a></td><td><code>{{.SHA}}</code></td></tr>{{else}}<tr><td>none</td></tr>{{end}}</table> |
| 7 | <table class="refs">{{range .Tags}}<tr><td class="name"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{.Name}}/">{{.Name}}</a></td><td class="sha"><code>{{short .SHA}}</code></td></tr>{{else}}<tr><td class="empty">none</td></tr>{{end}}</table> |
| 8 | 8 | {{end}} |
internal/web/templates/tree.html
+13 −6
| @@ -1,15 +1,22 @@ |
| 1 | 1 | {{define "title"}}{{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | 2 | {{define "content"}} |
| 3 | 3 | {{template "repoheader" .}} |
| 4 | | <p class="crumbs">{{.Ref}}: {{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}</p> |
| 5 | | <p class="clone">clone: <code>git clone {{.CloneURL}}</code></p> |
| 4 | <div class="pathbar"> |
| 5 | <span class="refchip">{{template "branchicon"}} {{.Ref}}</span> |
| 6 | <span class="crumbs"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>/{{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}</span> |
| 7 | <span class="spacer"></span> |
| 8 | <span class="clone">clone: <code>git clone {{.CloneURL}}</code></span> |
| 9 | </div> |
| 6 | 10 | <table class="tree"> |
| 7 | 11 | {{range .Entries}}<tr> |
| 12 | {{if eq .Type "tree"}}<td class="name"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{.Name}}/</a></td><td class="size"></td> |
| 13 | {{else}}<td class="name"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/blob/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{.Name}}</a></td><td class="size">{{.Size}}</td>{{end}} |
| 8 | 14 | <td class="mode">{{.Mode}}</td> |
| 9 | | {{if eq .Type "tree"}}<td><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/tree/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{.Name}}/</a></td><td></td> |
| 10 | | {{else}}<td><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/blob/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{.Name}}</a></td><td class="size">{{.Size}}</td>{{end}} |
| 11 | 15 | </tr> |
| 12 | | {{else}}<tr><td>empty</td></tr>{{end}} |
| 16 | {{else}}<tr><td class="name empty">this repository is empty — push something:<br><code>git remote add origin {{.CloneURL}}</code></td></tr>{{end}} |
| 13 | 17 | </table> |
| 14 | | {{if .ReadmeHTML}}<section class="readme">{{.ReadmeHTML}}</section>{{end}} |
| 18 | {{if .ReadmeHTML}}<section class="readme"> |
| 19 | <div class="cardhead">{{.ReadmeName}}</div> |
| 20 | <div class="rendered">{{.ReadmeHTML}}</div> |
| 21 | </section>{{end}} |
| 15 | 22 | {{end}} |
internal/web/web.go
+10 −1
| @@ -34,7 +34,16 @@ var version = sync.OnceValue(func() string { |
| 34 | 34 | return "" |
| 35 | 35 | }) |
| 36 | 36 | |
| 37 | | var funcs = template.FuncMap{"gitbayVersion": func() string { return version() }} |
| 37 | var funcs = template.FuncMap{ |
| 38 | "gitbayVersion": func() string { return version() }, |
| 39 | // short abbreviates a commit SHA for display. |
| 40 | "short": func(s string) string { |
| 41 | if len(s) > 10 { |
| 42 | return s[:10] |
| 43 | } |
| 44 | return s |
| 45 | }, |
| 46 | } |
| 38 | 47 | |
| 39 | 48 | // Render executes the named page template with the shared layout. |
| 40 | 49 | func Render(w io.Writer, page string, data any) error { |