Commit 542b37e732
Verified · cmc
Layout: unified · split
e2e/design_test.go added +66
| @@ -0,0 +1,66 @@ | |||
| 1 | package e2e | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "os" | ||
| 5 | "path/filepath" | ||
| 6 | "strings" | ||
| 7 | "testing" | ||
| 8 | ) | ||
| 9 | |||
| 10 | func 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\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 | ||
| 9 | const zeroSHA = "0000000000000000000000000000000000000000" | 9 | const zeroSHA = "0000000000000000000000000000000000000000" |
| 10 | 10 | ||
| 11 | // LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip, | ||
| 12 | // or "" for empty repos. | ||
| 13 | func 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. |
| 12 | func HasCommit(dir, sha string) bool { | 22 | func 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 @@ | |||
| 1 | package httpd | ||
| 2 | |||
| 3 | import ( | ||
| 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. | ||
| 15 | var 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 "". | ||
| 33 | func 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. | ||
| 54 | func 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. | ||
| 79 | type describedRepo struct { | 80 | type 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 | ||
| 84 | func (s *Server) describeAll(repos []store.Repo) []describedRepo { | 88 | func (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 | ||
| 379 | func (s *Server) blob(w http.ResponseWriter, r *http.Request) { | 397 | func (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); } |
| 204 | details.refmenu { position: relative; } | ||
| 205 | details.refmenu summary { cursor: pointer; list-style: none; } | ||
| 206 | details.refmenu summary::-webkit-details-marker { display: none; } | ||
| 207 | details.refmenu summary::after { content: " ▾"; color: var(--muted); } | ||
| 208 | details.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 | } | ||
| 222 | details.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 | } | ||
| 229 | details.refmenu .refdrop a:hover { background: var(--surface); text-decoration: none; } | ||
| 230 | details.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 */ |
| 206 | table.tree, table.refs { | 233 | table.tree, table.refs { |
| @@ -217,6 +244,17 @@ table.tree td, table.refs td { | |||
| 217 | } | 244 | } |
| 218 | table.tree tr:last-child td, table.refs tr:last-child td { border-bottom: none; } | 245 | table.tree tr:last-child td, table.refs tr:last-child td { border-bottom: none; } |
| 219 | table.tree tr:hover td, table.refs tr:hover td { background: var(--surface); } | 246 | table.tree tr:hover td, table.refs tr:hover td { background: var(--surface); } |
| 247 | table.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 | } | ||
| 256 | table.tree tr.cols:hover th { background: var(--surface); } | ||
| 257 | table.tree th.size { text-align: right; } | ||
| 220 | table.tree td.name { width: 100%; } | 258 | table.tree td.name { width: 100%; } |
| 221 | table.tree td.name a { color: var(--fg); } | 259 | table.tree td.name a { color: var(--fg); } |
| 222 | table.tree td.name a:hover { color: var(--accent); } | 260 | table.tree td.name a:hover { color: var(--accent); } |
| @@ -281,39 +319,27 @@ code.fullsha { color: var(--muted); overflow-wrap: anywhere; } | |||
| 281 | svg.icon { vertical-align: -0.125em; } | 319 | svg.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 { | 323 | ul.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); } | 331 | ul.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; } | 332 | ul.repolist li:last-child { border-bottom: none; } |
| 333 | ul.repolist li:hover { background: var(--surface); } | ||
| 334 | ul.repolist li.empty { color: var(--muted); } | ||
| 335 | ul.repolist p { margin: 0; } | ||
| 336 | ul.repolist .reponame { font-size: var(--fs-3); } | ||
| 337 | ul.repolist .reponame a { color: var(--fg); } | ||
| 338 | ul.repolist .reponame a strong { color: var(--accent); font-weight: 600; } | ||
| 339 | ul.repolist .reponame .sep { color: var(--muted); margin: 0 0.1em; } | ||
| 340 | ul.repolist .desc { margin-top: 0.1rem; } | ||
| 341 | ul.repolist .topics { margin-top: var(--sp-1); } | ||
| 342 | ul.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}} |