Commit 542b37e732

542b37e7324b23aec449b5fd296735fadffa3cdc

parent: 33f4fa3490

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24 23:04 UTC

web: repo rows, file table headers, README fixes, branch dropdown

Part of the #10 review round. Repo listings become one row per
repository (title with visibility/archived chips, description, clickable
topic chips linking filtered explore, then default branch · license ·
last updated — license detected from conventional files, date from the
branch tip). The files table gains column headers; the README card
header links to the file's blob page; relative links in rendered
READMEs resolve to blob pages (org/markdown .html exports map back to
their source files) and relative images to raw. The ref chip on tree
and blob pages becomes a no-JS dropdown listing branches with an
all-refs link.

Layout: unified · split

e2e/design_test.go added +66
@@ -0,0 +1,66 @@
1package e2e
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestReadmeRelativeLinks(t *testing.T) {
11 inst := startInstance(t)
12 aliceKey := inst.newKey(t, "alice")
13 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
14 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/site"); code != 0 {
15 t.Fatal("repo create failed")
16 }
17 work := t.TempDir()
18 env := inst.gitEnv(aliceKey)
19 mustGit(t, work, env, "clone", inst.sshURL("alice/site"), "w")
20 dir := filepath.Join(work, "w")
21 os.MkdirAll(filepath.Join(dir, "docs"), 0o755)
22 os.MkdirAll(filepath.Join(dir, "img"), 0o755)
23 os.WriteFile(filepath.Join(dir, "README.md"), []byte(
24 "# site\n\n[guide](docs/guide.md) and [export](docs/paper.html) and "+
25 "[abs](https://example.org/x) here\n\n![logo](img/logo.png)\n"), 0o644)
26 os.WriteFile(filepath.Join(dir, "docs", "guide.md"), []byte("# guide\n"), 0o644)
27 os.WriteFile(filepath.Join(dir, "docs", "paper.org"), []byte("* paper\n"), 0o644)
28 os.WriteFile(filepath.Join(dir, "img", "logo.png"), []byte{0x89, 0x50}, 0o644)
29 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
30 mustGit(t, dir, env, "add", ".")
31 mustGit(t, dir, env, "commit", "-q", "-m", "base")
32 mustGit(t, dir, env, "push", "-q", "origin", "main")
33
34 status, body := inst.get(t, "/alice/site")
35 if status != 200 {
36 t.Fatalf("tree: %d", status)
37 }
38 for _, want := range []string{
39 `href="/alice/site/blob/main/docs/guide.md"`, // relative link
40 `href="/alice/site/blob/main/docs/paper.org"`, // .html mapped to .org source
41 `src="/alice/site/raw/main/img/logo.png"`, // relative image via raw
42 `href="https://example.org/x"`, // absolute untouched
43 `href="/alice/site/blob/main/README.md">README.md</a>`, // clickable card header
44 `<th>name</th>`, // file table column headers
45 } {
46 if !strings.Contains(body, want) {
47 t.Errorf("missing %q", want)
48 }
49 }
50 // Branch dropdown lists branches.
51 if !strings.Contains(body, `class="refmenu"`) || !strings.Contains(body, ">all refs") {
52 t.Error("branch dropdown missing")
53 }
54 // Explore rows carry topics, license, and updated date.
55 inst.ssh(t, aliceKey, "", "repo", "topics", "add", "alice/site", "web")
56 os.WriteFile(filepath.Join(dir, "LICENSE"), []byte("Permission to use, copy, modify, and/or distribute this software...\n"), 0o644)
57 mustGit(t, dir, env, "add", ".")
58 mustGit(t, dir, env, "commit", "-q", "-m", "license")
59 mustGit(t, dir, env, "push", "-q", "origin", "main")
60 _, body = inst.get(t, "/explore")
61 for _, want := range []string{`href="/explore?q=web"`, "ISC", "updated 20"} {
62 if !strings.Contains(body, want) {
63 t.Errorf("explore row missing %q", want)
64 }
65 }
66}
internal/gitutil/messages.go +10
@@ -8,6 +8,16 @@ import (
8 8
9const zeroSHA = "0000000000000000000000000000000000000000" 9const zeroSHA = "0000000000000000000000000000000000000000"
10 10
11// LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip,
12// or "" for empty repos.
13func LastCommitDate(dir, ref string) string {
14 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%cs", ref).Output()
15 if err != nil {
16 return ""
17 }
18 return strings.TrimSpace(string(out))
19}
20
11// HasCommit reports whether sha names a commit object present in dir. 21// HasCommit reports whether sha names a commit object present in dir.
12func HasCommit(dir, sha string) bool { 22func HasCommit(dir, sha string) bool {
13 return exec.Command("git", "-C", dir, "cat-file", "-e", sha+"^{commit}").Run() == nil 23 return exec.Command("git", "-C", dir, "cat-file", "-e", sha+"^{commit}").Run() == nil
internal/httpd/readme.go added +108
@@ -0,0 +1,108 @@
1package httpd
2
3import (
4 "html/template"
5 "path"
6 "strings"
7
8 "golang.org/x/net/html"
9 "golang.org/x/net/html/atom"
10
11 "gitbay.org/gitbay/internal/gitutil"
12)
13
14// licenseNames maps content markers to display names, checked in order.
15var licenseChecks = []struct{ marker, name string }{
16 {"MIT License", "MIT"},
17 {"Permission is hereby granted, free of charge", "MIT"},
18 {"Apache License", "Apache-2.0"},
19 {"GNU AFFERO GENERAL PUBLIC LICENSE", "AGPL-3.0"},
20 {"GNU GENERAL PUBLIC LICENSE", "GPL"},
21 {"GNU LESSER GENERAL PUBLIC LICENSE", "LGPL"},
22 {"Mozilla Public License", "MPL-2.0"},
23 {"BSD 3-Clause", "BSD-3-Clause"},
24 {"BSD 2-Clause", "BSD-2-Clause"},
25 {"Redistribution and use in source and binary forms", "BSD"},
26 {"Permission to use, copy, modify, and/or distribute", "ISC"},
27 {"This is free and unencumbered software", "Unlicense"},
28 {"CC0", "CC0"},
29}
30
31// detectLicense reports the repo's license name from a conventional file
32// at the ref root, or "".
33func detectLicense(dir, ref string) string {
34 for _, name := range []string{"LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "UNLICENSE"} {
35 raw, err := gitutil.ReadBlob(dir, ref, name, 2048)
36 if err != nil {
37 continue
38 }
39 text := string(raw)
40 for _, c := range licenseChecks {
41 if strings.Contains(text, c.marker) {
42 return c.name
43 }
44 }
45 return "license"
46 }
47 return ""
48}
49
50// rewriteRelativeLinks makes relative hrefs and srcs in rendered repo
51// content resolve on the forge: links go to blob pages, images to raw.
52// go-org exports .org links as .html, so an .html target whose .org (or
53// .md) source exists in the tree maps back to the source file.
54func rewriteRelativeLinks(rendered template.HTML, p repoPage, baseDir string) template.HTML {
55 ctx := &html.Node{Type: html.ElementNode, Data: "div", DataAtom: atom.Div}
56 nodes, err := html.ParseFragment(strings.NewReader(string(rendered)), ctx)
57 if err != nil {
58 return rendered
59 }
60 var walk func(*html.Node)
61 walk = func(n *html.Node) {
62 if n.Type == html.ElementNode {
63 for i, a := range n.Attr {
64 isHref := a.Key == "href" && n.Data == "a"
65 isSrc := a.Key == "src" && (n.Data == "img" || n.Data == "video" || n.Data == "source")
66 if !isHref && !isSrc {
67 continue
68 }
69 v := a.Val
70 if v == "" || strings.Contains(v, "://") || strings.HasPrefix(v, "/") ||
71 strings.HasPrefix(v, "#") || strings.HasPrefix(v, "mailto:") ||
72 strings.HasPrefix(v, "data:") {
73 continue
74 }
75 target := path.Clean(path.Join(baseDir, v))
76 if strings.HasPrefix(target, "..") {
77 continue
78 }
79 if isSrc {
80 n.Attr[i].Val = "/" + p.Repo.Path() + "/raw/" + p.Ref + "/" + target
81 continue
82 }
83 // .html from org/markdown exports maps back to the source.
84 if strings.HasSuffix(target, ".html") {
85 stem := strings.TrimSuffix(target, ".html")
86 for _, ext := range []string{".org", ".md"} {
87 if _, err := gitutil.ReadBlob(p.Dir, p.Ref, stem+ext, 1); err == nil {
88 target = stem + ext
89 break
90 }
91 }
92 }
93 n.Attr[i].Val = "/" + p.Repo.Path() + "/blob/" + p.Ref + "/" + target
94 }
95 }
96 for c := n.FirstChild; c != nil; c = c.NextSibling {
97 walk(c)
98 }
99 }
100 var out strings.Builder
101 for _, n := range nodes {
102 walk(n)
103 if err := html.Render(&out, n); err != nil {
104 return rendered
105 }
106 }
107 return template.HTML(out.String())
108}
internal/httpd/web.go +31 −9
@@ -75,16 +75,28 @@ func (s *Server) notFound(w http.ResponseWriter, r *http.Request) {
75 buf.WriteTo(w) 75 buf.WriteTo(w)
76} 76}
77 77
78// describedRepo pairs a repo with its description for listings. 78// describedRepo pairs a repo with the listing metadata: description,
79// topics, license, and last-updated date.
79type describedRepo struct { 80type describedRepo struct {
80 store.Repo 81 store.Repo
81 Desc string 82 Desc string
83 Topics []string
84 License string
85 Updated string
82} 86}
83 87
84func (s *Server) describeAll(repos []store.Repo) []describedRepo { 88func (s *Server) describeAll(repos []store.Repo) []describedRepo {
85 var out []describedRepo 89 var out []describedRepo
86 for _, r := range repos { 90 for _, r := range repos {
87 out = append(out, describedRepo{r, gitutil.ReadDescription(control.RepoDir(s.cfg.Server.Root, r.OwnerName, r.Name))}) 91 dir := control.RepoDir(s.cfg.Server.Root, r.OwnerName, r.Name)
92 d := describedRepo{
93 Repo: r,
94 Desc: gitutil.ReadDescription(dir),
95 License: detectLicense(dir, r.DefaultBranch),
96 Updated: gitutil.LastCommitDate(dir, r.DefaultBranch),
97 }
98 d.Topics, _ = s.st.ListTopics(r.ID)
99 out = append(out, d)
88 } 100 }
89 return out 101 return out
90} 102}
@@ -182,8 +194,7 @@ func (s *Server) filterRepos(q string, repos []describedRepo) []describedRepo {
182 out = append(out, d) 194 out = append(out, d)
183 continue 195 continue
184 } 196 }
185 topics, _ := s.st.ListTopics(d.ID) 197 for _, t := range d.Topics {
186 for _, t := range topics {
187 if strings.Contains(t, q) { 198 if strings.Contains(t, q) {
188 out = append(out, d) 199 out = append(out, d)
189 break 200 break
@@ -342,10 +353,13 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage,
342 repoPage 353 repoPage
343 Crumbs []crumb 354 Crumbs []crumb
344 Prefix string 355 Prefix string
356 DirPath string
357 RefKind string
345 Entries []gitutil.TreeEntry 358 Entries []gitutil.TreeEntry
359 Branches []gitutil.Ref
346 ReadmeName string 360 ReadmeName string
347 ReadmeHTML template.HTML 361 ReadmeHTML template.HTML
348 }{repoPage: p}) 362 }{repoPage: p, RefKind: "tree"})
349 return 363 return
350 } 364 }
351 entries, err := gitutil.ListTree(p.Dir, p.Ref, dirPath) 365 entries, err := gitutil.ListTree(p.Dir, p.Ref, dirPath)
@@ -362,18 +376,22 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage,
362 readmeName := pickReadme(entries) 376 readmeName := pickReadme(entries)
363 if readmeName != "" { 377 if readmeName != "" {
364 if raw, err := gitutil.ReadBlob(p.Dir, p.Ref, prefix+readmeName, maxRenderBytes); err == nil { 378 if raw, err := gitutil.ReadBlob(p.Dir, p.Ref, prefix+readmeName, maxRenderBytes); err == nil {
365 readmeHTML = renderReadme(readmeName, raw) 379 readmeHTML = rewriteRelativeLinks(renderReadme(readmeName, raw), p, dirPath)
366 } 380 }
367 } 381 }
368 382
383 branches, _ := gitutil.Refs(p.Dir, "heads")
369 s.render(w, "tree.html", struct { 384 s.render(w, "tree.html", struct {
370 repoPage 385 repoPage
371 Crumbs []crumb 386 Crumbs []crumb
372 Prefix string 387 Prefix string
388 DirPath string
389 RefKind string
373 Entries []gitutil.TreeEntry 390 Entries []gitutil.TreeEntry
391 Branches []gitutil.Ref
374 ReadmeName string 392 ReadmeName string
375 ReadmeHTML template.HTML 393 ReadmeHTML template.HTML
376 }{p, crumbs(p, "tree", dirPath), prefix, entries, readmeName, readmeHTML}) 394 }{p, crumbs(p, "tree", dirPath), prefix, dirPath, "tree", entries, branches, readmeName, readmeHTML})
377} 395}
378 396
379func (s *Server) blob(w http.ResponseWriter, r *http.Request) { 397func (s *Server) blob(w http.ResponseWriter, r *http.Request) {
@@ -400,15 +418,19 @@ func (s *Server) blob(w http.ResponseWriter, r *http.Request) {
400 base = cs[len(cs)-1].Name 418 base = cs[len(cs)-1].Name
401 cs = cs[:len(cs)-1] 419 cs = cs[:len(cs)-1]
402 } 420 }
421 branches, _ := gitutil.Refs(p.Dir, "heads")
403 s.render(w, "blob.html", struct { 422 s.render(w, "blob.html", struct {
404 repoPage 423 repoPage
405 Crumbs []crumb 424 Crumbs []crumb
406 Base string 425 Base string
407 Path string 426 Path string
427 DirPath string
428 RefKind string
408 Binary bool 429 Binary bool
409 Size int 430 Size int
431 Branches []gitutil.Ref
410 CodeHTML template.HTML 432 CodeHTML template.HTML
411 }{p, cs, base, filePath, binary, len(data), codeHTML}) 433 }{p, cs, base, filePath, filePath, "blob", binary, len(data), branches, codeHTML})
412} 434}
413 435
414// releases lists tag-anchored releases with notes and assets. 436// releases lists tag-anchored releases with notes and assets.
internal/web/static/style.css +54 −28
@@ -201,6 +201,33 @@ nav.tabs a.active { color: var(--fg); border-bottom-color: var(--accent); font-w
201 color: var(--fg); 201 color: var(--fg);
202} 202}
203.pathbar .crumbs strong { color: var(--fg); } 203.pathbar .crumbs strong { color: var(--fg); }
204details.refmenu { position: relative; }
205details.refmenu summary { cursor: pointer; list-style: none; }
206details.refmenu summary::-webkit-details-marker { display: none; }
207details.refmenu summary::after { content: " ▾"; color: var(--muted); }
208details.refmenu .refdrop {
209 position: absolute;
210 z-index: 10;
211 top: calc(100% + 4px);
212 left: 0;
213 min-width: 12rem;
214 max-height: 20rem;
215 overflow-y: auto;
216 background: var(--bg);
217 border: 1px solid var(--line);
218 border-radius: var(--r-md);
219 box-shadow: 0 4px 16px rgba(0,0,0,0.12);
220 padding: var(--sp-1) 0;
221}
222details.refmenu .refdrop a {
223 display: block;
224 padding: var(--sp-1) var(--sp-3);
225 color: var(--fg);
226 font-family: var(--mono);
227 font-size: var(--fs-1);
228}
229details.refmenu .refdrop a:hover { background: var(--surface); text-decoration: none; }
230details.refmenu .refdrop a.allrefs { color: var(--accent); font-family: var(--sans); border-top: 1px solid var(--faint); margin-top: var(--sp-1); }
204 231
205/* file and refs tables: bordered cards, rows inside */ 232/* file and refs tables: bordered cards, rows inside */
206table.tree, table.refs { 233table.tree, table.refs {
@@ -217,6 +244,17 @@ table.tree td, table.refs td {
217} 244}
218table.tree tr:last-child td, table.refs tr:last-child td { border-bottom: none; } 245table.tree tr:last-child td, table.refs tr:last-child td { border-bottom: none; }
219table.tree tr:hover td, table.refs tr:hover td { background: var(--surface); } 246table.tree tr:hover td, table.refs tr:hover td { background: var(--surface); }
247table.tree tr.cols th {
248 text-align: left;
249 font-weight: 500;
250 font-size: var(--fs-1);
251 color: var(--muted);
252 padding: var(--sp-1) var(--sp-4);
253 border-bottom: 1px solid var(--line);
254 background: var(--surface);
255}
256table.tree tr.cols:hover th { background: var(--surface); }
257table.tree th.size { text-align: right; }
220table.tree td.name { width: 100%; } 258table.tree td.name { width: 100%; }
221table.tree td.name a { color: var(--fg); } 259table.tree td.name a { color: var(--fg); }
222table.tree td.name a:hover { color: var(--accent); } 260table.tree td.name a:hover { color: var(--accent); }
@@ -281,39 +319,27 @@ code.fullsha { color: var(--muted); overflow-wrap: anywhere; }
281svg.icon { vertical-align: -0.125em; } 319svg.icon { vertical-align: -0.125em; }
282.lede { font-size: var(--fs-3); margin: var(--sp-2) 0; } 320.lede { font-size: var(--fs-3); margin: var(--sp-2) 0; }
283 321
284/* repo listings (index, owner pages): responsive card grid */ 322/* repo listings (explore, owner pages, dashboard): one row per repo */
285.repogrid { 323ul.repolist {
286 display: grid; 324 list-style: none;
287 grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
288 gap: var(--sp-3);
289 margin: var(--sp-3) 0 var(--sp-5); 325 margin: var(--sp-3) 0 var(--sp-5);
290} 326 padding: 0;
291.repogrid .empty { color: var(--muted); grid-column: 1 / -1; }
292.repocard {
293 display: flex;
294 flex-direction: column;
295 gap: var(--sp-1);
296 border: 1px solid var(--line); 327 border: 1px solid var(--line);
297 border-radius: var(--r-lg); 328 border-radius: var(--r-lg);
298 padding: var(--sp-3) var(--sp-4);
299 min-width: 0;
300}
301.repocard p { margin: 0; }
302.repocard .reponame {
303 font-weight: 550;
304 overflow: hidden;
305 text-overflow: ellipsis;
306 white-space: nowrap;
307}
308.repocard .desc {
309 font-size: var(--fs-2);
310 display: -webkit-box;
311 -webkit-line-clamp: 2;
312 -webkit-box-orient: vertical;
313 overflow: hidden; 329 overflow: hidden;
314} 330}
315.repocard .meta { margin-top: auto; padding-top: var(--sp-1); } 331ul.repolist li { padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--faint); }
316.repocard .reponame .sep { color: var(--muted); margin: 0 0.1em; } 332ul.repolist li:last-child { border-bottom: none; }
333ul.repolist li:hover { background: var(--surface); }
334ul.repolist li.empty { color: var(--muted); }
335ul.repolist p { margin: 0; }
336ul.repolist .reponame { font-size: var(--fs-3); }
337ul.repolist .reponame a { color: var(--fg); }
338ul.repolist .reponame a strong { color: var(--accent); font-weight: 600; }
339ul.repolist .reponame .sep { color: var(--muted); margin: 0 0.1em; }
340ul.repolist .desc { margin-top: 0.1rem; }
341ul.repolist .topics { margin-top: var(--sp-1); }
342ul.repolist .meta { margin-top: var(--sp-1); }
317 343
318/* owner profile header */ 344/* owner profile header */
319.profilehead { 345.profilehead {
internal/web/templates/blob.html +1 −1
@@ -2,7 +2,7 @@
2{{define "content"}} 2{{define "content"}}
3{{template "repoheader" .}} 3{{template "repoheader" .}}
4<div class="pathbar"> 4<div class="pathbar">
5 <span class="refchip">{{template "branchicon"}} {{.Ref}}</span> 5 {{template "refmenu" .}}
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> 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> 7 <span class="spacer"></span>
8 <span class="actions">{{if not .Binary}}<a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/blame/{{.Ref}}/{{.Path}}">blame</a> · {{end}}<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> 8 <span class="actions">{{if not .Binary}}<a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/blame/{{.Ref}}/{{.Path}}">blame</a> · {{end}}<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>
internal/web/templates/dashboard.html +3 −7
@@ -6,14 +6,10 @@
6<p class="toolbar">logged in as <a href="/{{.Viewer}}">{{.Viewer}}</a></p> 6<p class="toolbar">logged in as <a href="/{{.Viewer}}">{{.Viewer}}</a></p>
7</div> 7</div>
8{{if .Pinned}}<h2>pinned</h2> 8{{if .Pinned}}<h2>pinned</h2>
9<div class="repogrid"> 9<ul class="repolist">
10{{range .Pinned}}<div class="repocard"> 10{{range .Pinned}}{{template "reporow" .}}
11 <p class="reponame"><a href="/{{.OwnerName}}">{{.OwnerName}}</a><span class="sep">/</span><a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a>{{if eq .Visibility "private"}} <span class="chip chip-neutral">private</span>{{end}}{{if .Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}</p>
12 {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}}
13 <p class="meta">{{template "branchicon"}} {{.DefaultBranch}}</p>
14</div>
15{{end}} 11{{end}}
16</div>{{end}} 12</ul>{{end}}
17<h2>open merge requests <span class="count">{{len .MRs}}</span></h2> 13<h2>open merge requests <span class="count">{{len .MRs}}</span></h2>
18<ul class="issuelist"> 14<ul class="issuelist">
19{{range .MRs}}<li> 15{{range .MRs}}<li>
internal/web/templates/explore.html +4 −8
@@ -7,12 +7,8 @@
7</form> 7</form>
8<span class="spacer"></span> 8<span class="spacer"></span>
9</div> 9</div>
10<div class="repogrid"> 10<ul class="repolist">
11{{range .Repos}}<div class="repocard"> 11{{range .Repos}}{{template "reporow" .}}
12 <p class="reponame"><a href="/{{.OwnerName}}">{{.OwnerName}}</a><span class="sep">/</span><a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a>{{if .Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}</p> 12{{else}}<li class="empty">no public repositories yet</li>{{end}}
13 {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}} 13</ul>
14 <p class="meta">{{template "branchicon"}} {{.DefaultBranch}}</p>
15</div>
16{{else}}<p class="empty">no public repositories yet</p>{{end}}
17</div>
18{{end}} 14{{end}}
internal/web/templates/layout.html +15
@@ -45,6 +45,21 @@
45</div> 45</div>
46{{end}} 46{{end}}
47 47
48{{define "reporow"}}<li>
49 <p class="reponame"><a href="/{{.OwnerName}}/{{.Name}}">{{.OwnerName}}<span class="sep">/</span><strong>{{.Name}}</strong></a>{{if eq .Visibility "private"}} <span class="chip chip-neutral">private</span>{{end}}{{if .Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}</p>
50 {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}}
51 {{if .Topics}}<p class="topics">{{range .Topics}}<a class="chip topic" href="/explore?q={{.}}">{{.}}</a> {{end}}</p>{{end}}
52 <p class="meta">{{template "branchicon"}} {{.DefaultBranch}}{{if .License}} · {{.License}}{{end}}{{if .Updated}} · updated {{.Updated}}{{end}}</p>
53</li>{{end}}
54
55{{define "refmenu"}}{{if .Branches}}<details class="refmenu">
56 <summary class="refchip">{{template "branchicon"}} {{.Ref}}</summary>
57 <div class="refdrop">
58 {{range .Branches}}<a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/{{$.RefKind}}/{{.Name}}/{{$.DirPath}}">{{.Name}}</a>
59 {{end}}<a class="allrefs" href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/refs">all refs →</a>
60 </div>
61</details>{{else}}<span class="refchip">{{template "branchicon"}} {{.Ref}}</span>{{end}}{{end}}
62
48{{define "branchicon"}}<svg class="icon" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM3.5 3.25a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>{{end}} 63{{define "branchicon"}}<svg class="icon" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM3.5 3.25a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/></svg>{{end}}
49 64
50{{define "sigbadge"}}<span class="badge badge-{{.State}}" title="{{.Fingerprint}}">{{.State}}{{if .Signer}} · {{.Signer}}{{end}}</span>{{end}} 65{{define "sigbadge"}}<span class="badge badge-{{.State}}" title="{{.Fingerprint}}">{{.State}}{{if .Signer}} · {{.Signer}}{{end}}</span>{{end}}
internal/web/templates/owner.html +4 −8
@@ -8,12 +8,8 @@
8{{if .Members}}<p class="meta">members {{range .Members}}<a class="memberchip" href="/{{.Username}}">{{.Username}} <span class="role">{{.Role}}</span></a> {{end}}</p>{{end}} 8{{if .Members}}<p class="meta">members {{range .Members}}<a class="memberchip" href="/{{.Username}}">{{.Username}} <span class="role">{{.Role}}</span></a> {{end}}</p>{{end}}
9</section> 9</section>
10<h2>repositories <span class="count">{{len .Repos}}</span></h2> 10<h2>repositories <span class="count">{{len .Repos}}</span></h2>
11<div class="repogrid"> 11<ul class="repolist">
12{{range .Repos}}<div class="repocard"> 12{{range .Repos}}{{template "reporow" .}}
13 <p class="reponame"><a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a>{{if eq .Visibility "private"}} <span class="chip chip-neutral">private</span>{{end}}{{if .Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}</p> 13{{else}}<li class="empty">no visible repositories</li>{{end}}
14 {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}} 14</ul>
15 <p class="meta">{{template "branchicon"}} {{.DefaultBranch}}</p>
16</div>
17{{else}}<p class="empty">no visible repositories</p>{{end}}
18</div>
19{{end}} 15{{end}}
internal/web/templates/tree.html +4 −3
@@ -2,21 +2,22 @@
2{{define "content"}} 2{{define "content"}}
3{{template "repoheader" .}} 3{{template "repoheader" .}}
4<div class="pathbar"> 4<div class="pathbar">
5 <span class="refchip">{{template "branchicon"}} {{.Ref}}</span> 5 {{template "refmenu" .}}
6 <span class="crumbs"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>/{{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}</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> 7 <span class="spacer"></span>
8 <span class="clone">clone: <code>git clone {{.CloneURL}}</code></span> 8 <span class="clone">clone: <code>git clone {{.CloneURL}}</code></span>
9</div> 9</div>
10<table class="tree"> 10<table class="tree">
11<tr class="cols"><th>name</th><th class="size">size</th><th class="mode">mode</th></tr>
11{{range .Entries}}<tr> 12{{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 {{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}} 14 {{else}}<td class="name"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/blob/{{$.Ref}}/{{$.Prefix}}{{.Name}}">{{.Name}}</a></td><td class="size">{{.Size}}</td>{{end}}
14 <td class="mode">{{.Mode}}</td> 15 <td class="mode">{{.Mode}}</td>
15</tr> 16</tr>
16{{else}}<tr><td class="name empty">this repository is empty — push something:<br><code>git remote add origin {{.CloneURL}}</code></td></tr>{{end}} 17{{else}}<tr><td class="name empty" colspan="3">this repository is empty — push something:<br><code>git remote add origin {{.CloneURL}}</code></td></tr>{{end}}
17</table> 18</table>
18{{if .ReadmeHTML}}<section class="readme"> 19{{if .ReadmeHTML}}<section class="readme">
19<div class="cardhead">{{.ReadmeName}}</div> 20<div class="cardhead"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/blob/{{.Ref}}/{{.Prefix}}{{.ReadmeName}}">{{.ReadmeName}}</a></div>
20<div class="rendered">{{.ReadmeHTML}}</div> 21<div class="rendered">{{.ReadmeHTML}}</div>
21</section>{{end}} 22</section>{{end}}
22{{end}} 23{{end}}