Commit 9bf001e417
9bf001e41785e5c85d396154fe8d5cb2da235c14
parent: 51cfb502fd
Verified · cmc
cmc <hello@cleberg.net> · 2026-08-24 16:47 UTC
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.
Layout: unified · split
internal/httpd/web.go
+7 −4
| @@ -243,6 +243,7 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, |
| 243 | Crumbs []crumb |
243 | Crumbs []crumb |
| 244 | Prefix string |
244 | Prefix string |
| 245 | Entries []gitutil.TreeEntry |
245 | Entries []gitutil.TreeEntry |
| |
246 | ReadmeName string |
| 246 | ReadmeHTML template.HTML |
247 | ReadmeHTML template.HTML |
| 247 | }{repoPage: p}) |
248 | }{repoPage: p}) |
| 248 | return |
249 | return |
| @@ -258,9 +259,10 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, |
| 258 | } |
259 | } |
| 259 | |
260 | |
| 260 | var readmeHTML template.HTML |
261 | var readmeHTML template.HTML |
| 261 | if name := pickReadme(entries); name != "" { |
262 | readmeName := pickReadme(entries) |
| 262 | if raw, err := gitutil.ReadBlob(p.Dir, p.Ref, prefix+name, maxRenderBytes); err == nil { |
263 | if readmeName != "" { |
| 263 | readmeHTML = renderReadme(name, raw) |
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 | Crumbs []crumb |
271 | Crumbs []crumb |
| 270 | Prefix string |
272 | Prefix string |
| 271 | Entries []gitutil.TreeEntry |
273 | Entries []gitutil.TreeEntry |
| |
274 | ReadmeName string |
| 272 | ReadmeHTML template.HTML |
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 | func (s *Server) blob(w http.ResponseWriter, r *http.Request) { |
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 | nav.tabs a:hover { color: var(--fg); background: var(--surface); text-decoration: none; } |
150 | nav.tabs a:hover { color: var(--fg); background: var(--surface); text-decoration: none; } |
| 151 | nav.tabs a.active { color: var(--fg); border-bottom-color: var(--accent); font-weight: 550; } |
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 | background: var(--surface); |
155 | background: var(--surface); |
| 155 | border: 1px solid var(--faint); |
156 | border: 1px solid var(--faint); |
| 156 | padding: 0.15rem 0.5rem; |
157 | padding: 0.15rem 0.5rem; |
| 157 | border-radius: var(--r-sm); |
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 | .crumbs { color: var(--muted); font-size: var(--fs-2); } |
234 | .crumbs { color: var(--muted); font-size: var(--fs-2); } |
| 160 | .desc, td.desc { color: var(--muted); } |
235 | .desc, td.desc { color: var(--muted); } |
| 161 | .error { color: var(--bad); } |
236 | .error { color: var(--bad); } |
internal/web/templates/blob.html
+7 −3
| @@ -1,8 +1,12 @@ |
| 1 | {{define "title"}}{{.Path}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
1 | {{define "title"}}{{.Path}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | {{define "content"}} |
2 | {{define "content"}} |
| 3 | {{template "repoheader" .}} |
3 | {{template "repoheader" .}} |
| 4 | <p class="crumbs">{{.Ref}}: {{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}{{.Base}} |
4 | <div class="pathbar"> |
| 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> |
5 | <span class="refchip">{{template "branchicon"}} {{.Ref}}</span> |
| 6 | {{if .Binary}}<p>binary file, {{.Size}} bytes — <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/raw/{{.Ref}}/{{.Path}}">download</a></p> |
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 | {{else}}<div class="code">{{.CodeHTML}}</div>{{end}} |
11 | {{else}}<div class="code">{{.CodeHTML}}</div>{{end}} |
| 8 | {{end}} |
12 | {{end}} |
internal/web/templates/commit.html
+7 −4
| @@ -1,10 +1,13 @@ |
| 1 | {{define "title"}}{{.ShortSHA}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
1 | {{define "title"}}{{.ShortSHA}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | {{define "content"}} |
2 | {{define "content"}} |
| 3 | {{template "repoheader" .}} |
3 | {{template "repoheader" .}} |
| 4 | <h2><code>{{.SHA}}</code></h2> |
4 | <div class="commithead"> |
| 5 | <p>{{template "sigbadge" .Sig}}{{range .Checks}} <span class="badge check-{{.State}}">{{.Context}}: {{.State}}</span>{{end}}</p> |
5 | <h2>commit <code>{{.ShortSHA}}</code></h2> |
| 6 | <p>author: {{.AuthorName}} <{{.AuthorEmail}}> · {{.Date}} |
6 | <p class="meta"><code class="fullsha">{{.SHA}}</code></p> |
| 7 | {{if .CommitterEmail}}<br>committer: <{{.CommitterEmail}}>{{end}}</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 | <pre class="message">{{.Message}}</pre> |
11 | <pre class="message">{{.Message}}</pre> |
| 9 | <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span> |
12 | <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span> |
| 10 | {{end}}</pre> |
13 | {{end}}</pre> |
internal/web/templates/log.html
+14 −11
| @@ -1,15 +1,18 @@ |
| 1 | {{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
1 | {{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | {{define "content"}} |
2 | {{define "content"}} |
| 3 | {{template "repoheader" .}} |
3 | {{template "repoheader" .}} |
| 4 | <table class="log"> |
4 | <ul class="loglist"> |
| 5 | {{range .Commits}}<tr> |
5 | {{range .Commits}}<li> |
| 6 | <td><code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code></td> |
6 | <div class="commitmain"> |
| 7 | <td>{{.Subject}}</td> |
7 | <p class="subject"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.Subject}}</a></p> |
| 8 | <td>{{.AuthorName}} <{{.AuthorEmail}}></td> |
8 | <p class="meta"><span title="{{.AuthorEmail}}">{{.AuthorName}}</span> · {{.Date}}</p> |
| 9 | <td>{{.Date}}</td> |
9 | </div> |
| 10 | <td>{{template "sigbadge" .Sig}}</td> |
10 | <div class="commitside"> |
| 11 | </tr> |
11 | {{template "sigbadge" .Sig}} |
| 12 | {{end}} |
12 | <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.SHA}}">{{.ShortSHA}}</a></code> |
| 13 | </table> |
13 | </div> |
| 14 | {{if .NextSHA}}<p><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log/{{.NextSHA}}">older →</a></p>{{end}} |
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 | {{end}} |
18 | {{end}} |
internal/web/templates/refs.html
+2 −2
| @@ -2,7 +2,7 @@ |
| 2 | {{define "content"}} |
2 | {{define "content"}} |
| 3 | {{template "repoheader" .}} |
3 | {{template "repoheader" .}} |
| 4 | <h2>branches</h2> |
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 | <h2>tags</h2> |
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 | {{end}} |
8 | {{end}} |
internal/web/templates/tree.html
+13 −6
| @@ -1,15 +1,22 @@ |
| 1 | {{define "title"}}{{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
1 | {{define "title"}}{{.Repo.OwnerName}}/{{.Repo.Name}}{{end}} |
| 2 | {{define "content"}} |
2 | {{define "content"}} |
| 3 | {{template "repoheader" .}} |
3 | {{template "repoheader" .}} |
| 4 | <p class="crumbs">{{.Ref}}: {{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}</p> |
4 | <div class="pathbar"> |
| 5 | <p class="clone">clone: <code>git clone {{.CloneURL}}</code></p> |
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 | <table class="tree"> |
10 | <table class="tree"> |
| 7 | {{range .Entries}}<tr> |
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 | <td class="mode">{{.Mode}}</td> |
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 | </tr> |
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 | </table> |
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 | {{end}} |
22 | {{end}} |
internal/web/web.go
+10 −1
| @@ -34,7 +34,16 @@ var version = sync.OnceValue(func() string { |
| 34 | return "" |
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 | // Render executes the named page template with the shared layout. |
48 | // Render executes the named page template with the shared layout. |
| 40 | func Render(w io.Writer, page string, data any) error { |
49 | func Render(w io.Writer, page string, data any) error { |