Commit 542b37e732
Verified · cmc
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 | 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 | 21 | // HasCommit reports whether sha names a commit object present in dir. |
| 12 | 22 | func HasCommit(dir, sha string) bool { |
| 13 | 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 | 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 | 80 | type describedRepo struct { |
| 80 | 81 | store.Repo |
| 81 | Desc string | |
| 82 | Desc string | |
| 83 | Topics []string | |
| 84 | License string | |
| 85 | Updated string | |
| 82 | 86 | } |
| 83 | 87 | |
| 84 | 88 | func (s *Server) describeAll(repos []store.Repo) []describedRepo { |
| 85 | 89 | var out []describedRepo |
| 86 | 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 | 101 | return out |
| 90 | 102 | } |
| @@ -182,8 +194,7 @@ func (s *Server) filterRepos(q string, repos []describedRepo) []describedRepo { | ||
| 182 | 194 | out = append(out, d) |
| 183 | 195 | continue |
| 184 | 196 | } |
| 185 | topics, _ := s.st.ListTopics(d.ID) | |
| 186 | for _, t := range topics { | |
| 197 | for _, t := range d.Topics { | |
| 187 | 198 | if strings.Contains(t, q) { |
| 188 | 199 | out = append(out, d) |
| 189 | 200 | break |
| @@ -342,10 +353,13 @@ func (s *Server) renderTree(w http.ResponseWriter, r *http.Request, p repoPage, | ||
| 342 | 353 | repoPage |
| 343 | 354 | Crumbs []crumb |
| 344 | 355 | Prefix string |
| 356 | DirPath string | |
| 357 | RefKind string | |
| 345 | 358 | Entries []gitutil.TreeEntry |
| 359 | Branches []gitutil.Ref | |
| 346 | 360 | ReadmeName string |
| 347 | 361 | ReadmeHTML template.HTML |
| 348 | }{repoPage: p}) | |
| 362 | }{repoPage: p, RefKind: "tree"}) | |
| 349 | 363 | return |
| 350 | 364 | } |
| 351 | 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 | 376 | readmeName := pickReadme(entries) |
| 363 | 377 | if readmeName != "" { |
| 364 | 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 | 384 | s.render(w, "tree.html", struct { |
| 370 | 385 | repoPage |
| 371 | 386 | Crumbs []crumb |
| 372 | 387 | Prefix string |
| 388 | DirPath string | |
| 389 | RefKind string | |
| 373 | 390 | Entries []gitutil.TreeEntry |
| 391 | Branches []gitutil.Ref | |
| 374 | 392 | ReadmeName string |
| 375 | 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 | 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 | 418 | base = cs[len(cs)-1].Name |
| 401 | 419 | cs = cs[:len(cs)-1] |
| 402 | 420 | } |
| 421 | branches, _ := gitutil.Refs(p.Dir, "heads") | |
| 403 | 422 | s.render(w, "blob.html", struct { |
| 404 | 423 | repoPage |
| 405 | 424 | Crumbs []crumb |
| 406 | 425 | Base string |
| 407 | 426 | Path string |
| 427 | DirPath string | |
| 428 | RefKind string | |
| 408 | 429 | Binary bool |
| 409 | 430 | Size int |
| 431 | Branches []gitutil.Ref | |
| 410 | 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 | 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 | 201 | color: var(--fg); |
| 202 | 202 | } |
| 203 | 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 | 232 | /* file and refs tables: bordered cards, rows inside */ |
| 206 | 233 | table.tree, table.refs { |
| @@ -217,6 +244,17 @@ table.tree td, table.refs td { | ||
| 217 | 244 | } |
| 218 | 245 | table.tree tr:last-child td, table.refs tr:last-child td { border-bottom: none; } |
| 219 | 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 | 258 | table.tree td.name { width: 100%; } |
| 221 | 259 | table.tree td.name a { color: var(--fg); } |
| 222 | 260 | table.tree td.name a:hover { color: var(--accent); } |
| @@ -281,39 +319,27 @@ code.fullsha { color: var(--muted); overflow-wrap: anywhere; } | ||
| 281 | 319 | svg.icon { vertical-align: -0.125em; } |
| 282 | 320 | .lede { font-size: var(--fs-3); margin: var(--sp-2) 0; } |
| 283 | 321 | |
| 284 | /* repo listings (index, owner pages): responsive card grid */ | |
| 285 | .repogrid { | |
| 286 | display: grid; | |
| 287 | grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); | |
| 288 | gap: var(--sp-3); | |
| 322 | /* repo listings (explore, owner pages, dashboard): one row per repo */ | |
| 323 | ul.repolist { | |
| 324 | list-style: none; | |
| 289 | 325 | margin: var(--sp-3) 0 var(--sp-5); |
| 290 | } | |
| 291 | .repogrid .empty { color: var(--muted); grid-column: 1 / -1; } | |
| 292 | .repocard { | |
| 293 | display: flex; | |
| 294 | flex-direction: column; | |
| 295 | gap: var(--sp-1); | |
| 326 | padding: 0; | |
| 296 | 327 | border: 1px solid var(--line); |
| 297 | 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 | 329 | overflow: hidden; |
| 314 | 330 | } |
| 315 | .repocard .meta { margin-top: auto; padding-top: var(--sp-1); } | |
| 316 | .repocard .reponame .sep { color: var(--muted); margin: 0 0.1em; } | |
| 331 | ul.repolist li { padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--faint); } | |
| 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 | 344 | /* owner profile header */ |
| 319 | 345 | .profilehead { |
internal/web/templates/blob.html +1 −1
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | {{define "content"}} |
| 3 | 3 | {{template "repoheader" .}} |
| 4 | 4 | <div class="pathbar"> |
| 5 | <span class="refchip">{{template "branchicon"}} {{.Ref}}</span> | |
| 5 | {{template "refmenu" .}} | |
| 6 | 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 | 7 | <span class="spacer"></span> |
| 8 | 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 | 6 | <p class="toolbar">logged in as <a href="/{{.Viewer}}">{{.Viewer}}</a></p> |
| 7 | 7 | </div> |
| 8 | 8 | {{if .Pinned}}<h2>pinned</h2> |
| 9 | <div class="repogrid"> | |
| 10 | {{range .Pinned}}<div class="repocard"> | |
| 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> | |
| 9 | <ul class="repolist"> | |
| 10 | {{range .Pinned}}{{template "reporow" .}} | |
| 15 | 11 | {{end}} |
| 16 | </div>{{end}} | |
| 12 | </ul>{{end}} | |
| 17 | 13 | <h2>open merge requests <span class="count">{{len .MRs}}</span></h2> |
| 18 | 14 | <ul class="issuelist"> |
| 19 | 15 | {{range .MRs}}<li> |
internal/web/templates/explore.html +4 −8
| @@ -7,12 +7,8 @@ | ||
| 7 | 7 | </form> |
| 8 | 8 | <span class="spacer"></span> |
| 9 | 9 | </div> |
| 10 | <div class="repogrid"> | |
| 11 | {{range .Repos}}<div class="repocard"> | |
| 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> | |
| 13 | {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}} | |
| 14 | <p class="meta">{{template "branchicon"}} {{.DefaultBranch}}</p> | |
| 15 | </div> | |
| 16 | {{else}}<p class="empty">no public repositories yet</p>{{end}} | |
| 17 | </div> | |
| 10 | <ul class="repolist"> | |
| 11 | {{range .Repos}}{{template "reporow" .}} | |
| 12 | {{else}}<li class="empty">no public repositories yet</li>{{end}} | |
| 13 | </ul> | |
| 18 | 14 | {{end}} |
internal/web/templates/layout.html +15
| @@ -45,6 +45,21 @@ | ||
| 45 | 45 | </div> |
| 46 | 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 | 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 | 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 | 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 | 9 | </section> |
| 10 | 10 | <h2>repositories <span class="count">{{len .Repos}}</span></h2> |
| 11 | <div class="repogrid"> | |
| 12 | {{range .Repos}}<div class="repocard"> | |
| 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> | |
| 14 | {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}} | |
| 15 | <p class="meta">{{template "branchicon"}} {{.DefaultBranch}}</p> | |
| 16 | </div> | |
| 17 | {{else}}<p class="empty">no visible repositories</p>{{end}} | |
| 18 | </div> | |
| 11 | <ul class="repolist"> | |
| 12 | {{range .Repos}}{{template "reporow" .}} | |
| 13 | {{else}}<li class="empty">no visible repositories</li>{{end}} | |
| 14 | </ul> | |
| 19 | 15 | {{end}} |
internal/web/templates/tree.html +4 −3
| @@ -2,21 +2,22 @@ | ||
| 2 | 2 | {{define "content"}} |
| 3 | 3 | {{template "repoheader" .}} |
| 4 | 4 | <div class="pathbar"> |
| 5 | <span class="refchip">{{template "branchicon"}} {{.Ref}}</span> | |
| 5 | {{template "refmenu" .}} | |
| 6 | 6 | <span class="crumbs"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>/{{range .Crumbs}}<a href="{{.URL}}">{{.Name}}</a>/{{end}}</span> |
| 7 | 7 | <span class="spacer"></span> |
| 8 | 8 | <span class="clone">clone: <code>git clone {{.CloneURL}}</code></span> |
| 9 | 9 | </div> |
| 10 | 10 | <table class="tree"> |
| 11 | <tr class="cols"><th>name</th><th class="size">size</th><th class="mode">mode</th></tr> | |
| 11 | 12 | {{range .Entries}}<tr> |
| 12 | 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 | 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 | 15 | <td class="mode">{{.Mode}}</td> |
| 15 | 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 | 18 | </table> |
| 18 | 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 | 21 | <div class="rendered">{{.ReadmeHTML}}</div> |
| 21 | 22 | </section>{{end}} |
| 22 | 23 | {{end}} |