A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

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,
243243 Crumbs []crumb
244244 Prefix string
245245 Entries []gitutil.TreeEntry
246 ReadmeName string
246247 ReadmeHTML template.HTML
247248 }{repoPage: p})
248249 return
@@ -258,9 +259,10 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage,
258259 }
259260
260261 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)
264266 }
265267 }
266268
@@ -269,8 +271,9 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage,
269271 Crumbs []crumb
270272 Prefix string
271273 Entries []gitutil.TreeEntry
274 ReadmeName string
272275 ReadmeHTML template.HTML
273 }{p, crumbs(p, "tree", dirPath), prefix, entries, readmeHTML})
276 }{p, crumbs(p, "tree", dirPath), prefix, entries, readmeName, readmeHTML})
274277 }
275278
276279 func (s *Server) blob(w http.ResponseWriter, r *http.Request) {
internal/web/static/style.css +76 −1
@@ -150,12 +150,87 @@ nav.tabs a {
150150 nav.tabs a:hover { color: var(--fg); background: var(--surface); text-decoration: none; }
151151 nav.tabs a.active { color: var(--fg); border-bottom-color: var(--accent); font-weight: 550; }
152152
153p.clone code {
153.clone { color: var(--muted); font-size: var(--fs-2); }
154.clone code {
154155 background: var(--surface);
155156 border: 1px solid var(--faint);
156157 padding: 0.15rem 0.5rem;
157158 border-radius: var(--r-sm);
158159 }
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 */
186table.tree td.name { width: 100%; }
187table.tree td.name a { color: var(--fg); }
188table.tree td.name a:hover { color: var(--accent); }
189table.tree td.size { text-align: right; }
190table.tree td.empty, table.refs td.empty, li.empty, ul.loglist li.empty { color: var(--muted); }
191
192/* refs page */
193table.refs td.name { width: 100%; }
194table.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 */
209ul.loglist { list-style: none; margin: var(--sp-3) 0; padding: 0; }
210ul.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}
217ul.loglist .commitmain { flex: 1; min-width: 0; }
218ul.loglist p { margin: 0; }
219ul.loglist .subject a { color: var(--fg); font-weight: 550; }
220ul.loglist .subject a:hover { color: var(--accent); }
221ul.loglist .commitside {
222 display: flex;
223 align-items: baseline;
224 gap: var(--sp-2);
225 white-space: nowrap;
226}
227p.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); }
232code.fullsha { color: var(--muted); overflow-wrap: anywhere; }
233.empty-note { color: var(--muted); }
159234 .crumbs { color: var(--muted); font-size: var(--fs-2); }
160235 .desc, td.desc { color: var(--muted); }
161236 .error { color: var(--bad); }
internal/web/templates/blob.html +7 −3
@@ -1,8 +1,12 @@
11 {{define "title"}}{{.Path}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
33 {{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>
711 {{else}}<div class="code">{{.CodeHTML}}</div>{{end}}
812 {{end}}
internal/web/templates/commit.html +7 −4
@@ -1,10 +1,13 @@
11 {{define "title"}}{{.ShortSHA}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
33 {{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}} &lt;{{.AuthorEmail}}&gt; · {{.Date}}
7{{if .CommitterEmail}}<br>committer: &lt;{{.CommitterEmail}}&gt;{{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> &lt;{{.AuthorEmail}}&gt; · {{.Date}}
9 {{if .CommitterEmail}}<br>committer: &lt;{{.CommitterEmail}}&gt;{{end}}</p>
10</div>
811 <pre class="message">{{.Message}}</pre>
912 <pre class="diff">{{range .DiffLines}}<span class="{{.Class}}">{{.Text}}</span>
1013 {{end}}</pre>
internal/web/templates/log.html +14 −11
@@ -1,15 +1,18 @@
11 {{define "title"}}log · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
33 {{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}} &lt;{{.AuthorEmail}}&gt;</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}}
1518 {{end}}
internal/web/templates/refs.html +2 −2
@@ -2,7 +2,7 @@
22 {{define "content"}}
33 {{template "repoheader" .}}
44 <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>
66 <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>
88 {{end}}
internal/web/templates/tree.html +13 −6
@@ -1,15 +1,22 @@
11 {{define "title"}}{{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22 {{define "content"}}
33 {{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>
610 <table class="tree">
711 {{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}}
814 <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}}
1115 </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}}
1317 </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}}
1522 {{end}}
internal/web/web.go +10 −1
@@ -34,7 +34,16 @@ var version = sync.OnceValue(func() string {
3434 return ""
3535 })
3636
37var funcs = template.FuncMap{"gitbayVersion": func() string { return version() }}
37var 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}
3847
3948 // Render executes the named page template with the shared layout.
4049 func Render(w io.Writer, page string, data any) error {