~gitpocket/gitpocket

feat(web): order repos by last push, link authors, serve media and render readme video

commit
099b7551ea4bdf1874bee2e9873b0eb6b8a6cf8c browse tree
parent
e25b6c8474f332e0ea5e97f3115d1fb8c1ffb252
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-11-13 05:42 UTC

10 files changed, +54 −28

static/style.css modified

@@ -67,8 +67,6 @@ body.fluid main.file{padding-top:.9rem}
6767 .descbar .inner{padding:.3rem 1rem}
6868 .repohead+main{margin-top:0}
6969 main{padding-bottom:3rem}
70-.welcome{max-width:50rem;margin:0 auto 1.5rem;padding:0 1rem}
71-.welcome p{margin:.3rem 0 0}
7270 .cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;margin-bottom:1.5rem}
7371 .panel{background:var(--soft);padding:.8rem 1rem;margin-bottom:1.5rem}
7472 .panel.narrow{max-width:34rem;margin:1rem auto}

store.go modified

@@ -605,14 +605,6 @@ func (s *Store) ReposByOwner(owner int64, includePrivate bool) ([]Repo, error) {
605605 return scanRepos(rows)
606606 }
607607
608-func (s *Store) RecentRepos(n int) ([]Repo, error) {
609- rows, err := s.db.Query(`select `+repoColumns+` where r.private = 0 order by r.created desc limit ?`, n)
610- if err != nil {
611- return nil, err
612- }
613- return scanRepos(rows)
614-}
615-
616608 func (s *Store) UpdateRepo(id int64, description string, visibility int, rawTags string) error {
617609 tags, err := parseTags(rawTags)
618610 if err != nil {

templates/commit.html modified

@@ -6,7 +6,7 @@
66 <dl class="meta">
77 <dt>commit</dt><dd><code>{{$c.Hash}}</code> <a href="/{{$p.Repo.Path}}/tree/{{$c.Hash}}/">browse tree</a></dd>
88 {{range $c.Parents}}<dt>parent</dt><dd><a href="/{{$p.Repo.Path}}/commit/{{.}}"><code>{{.}}</code></a></dd>{{end}}
9-<dt>author</dt><dd>{{$c.Author}} &lt;{{$c.Email}}&gt;</dd>
9+<dt>author</dt><dd>{{template "who" $c}} &lt;{{$c.Email}}&gt;</dd>
1010 <dt>signature</dt><dd>{{with signer $c}}<span class="verified">Verified ~{{.}}</span> signed with a key registered by <a href="/~{{.}}">~{{.}}</a>{{else}}{{if and $c.Signature (ne $c.Signature "N")}}<span class="verified unknown">Unverified</span> signed with an unregistered key{{else}}none{{end}}{{end}}</dd>
1111 <dt>date</dt><dd>{{date $c.Time}} UTC</dd>
1212 </dl>

templates/compare.html modified

@@ -11,7 +11,7 @@
1111 {{if .Data.Unrelated}}<p class="info">These refs have no common history.</p>{{end}}
1212 {{if .Data.Commits}}
1313 <h3>Commits <span class="muted">{{len .Data.Commits}}</span></h3>
14-<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a></div><div class="muted small">{{.Author}} · {{ago .Time}}</div></div>{{end}}</div>
14+<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a></div><div class="muted small">{{template "who" .}} · {{ago .Time}}</div></div>{{end}}</div>
1515 {{else if and .Data.Base .Data.Head (not .Data.Unrelated)}}<p class="info">{{.Data.Head}} has nothing that {{.Data.Base}} does not.</p>{{end}}
1616 {{if .Data.Files}}
1717 <h3>Changes <span class="muted">{{len .Data.Files}} files, <span class="add">+{{.Data.Added}}</span> <span class="del">−{{.Data.Removed}}</span></span></h3>

templates/index.html modified

@@ -9,10 +9,6 @@
99 <section class="panel"><h3>Keys and tokens</h3><p>Register SSH public keys for pushing and create access tokens for HTTPS pushes or scripts.</p><a class="btn" href="/settings/keys">Manage keys ▸</a></section>
1010 </div>
1111 {{else}}
12-<div class="welcome">
13-<strong>Welcome to {{.Host}}!</strong>
14-<p>{{.Host}} is a small, self hosted git forge. It serves repositories over SSH and HTTPS and renders trees, logs, diffs and READMEs as plain pages, with Markdown and LaTeX where it helps and no scripts where it doesn't.</p>
15-</div>
1612 <div class="cards">
1713 <section class="panel"><h3>Host a repository</h3><p>Registration is by invitation. Once you have an account, push any repository and it gets a page with its history, refs and README.</p><a class="btn primary" href="/register">Register ▸</a></section>
1814 <section class="panel"><h3>Browse the code</h3><p>Every public repository can be cloned anonymously over HTTPS and read in the browser with syntax highlighting.</p><a class="btn" href="/projects">Browse public projects ▸</a></section>

templates/log.html modified

@@ -2,7 +2,7 @@
22 {{$p := .Data.Page}}
33 {{if .Data.Path}}<p class="muted">History of <a href="/{{$p.Repo.Path}}/tree/{{$p.Ref}}/{{.Data.Path}}">{{.Data.Path}}</a> on {{$p.Ref}}</p>{{end}}
44 {{if $p.Empty}}<p class="muted">No commits.</p>{{else}}
5-<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a>{{template "badge" .}}</div><div class="muted small">{{.Author}} · <span title="{{date .Time}}">{{ago .Time}}</span></div>{{if .Body}}<pre class="body">{{.Body}}</pre>{{end}}</div>{{end}}</div>
5+<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a>{{template "badge" .}}</div><div class="muted small">{{template "who" .}} · <span title="{{date .Time}}">{{ago .Time}}</span></div>{{if .Body}}<pre class="body">{{.Body}}</pre>{{end}}</div>{{end}}</div>
66 {{with .Data.Next}}<p><a class="btn" href="?from={{.}}{{if $.Data.Path}}&path={{$.Data.Path}}{{end}}">Older ▸</a></p>{{end}}
77 {{end}}
88 {{end}}

templates/meta.html modified

@@ -8,3 +8,4 @@
88 {{else if .Assignees}}{{range .Assignees}}<a href="/~{{.Name}}">~{{.Name}}</a> {{end}}{{else}}<p class="muted small">Nobody</p>{{end}}
99 </div>
1010 {{end}}
11+{{define "who"}}{{with person .Email}}<a href="/~{{.}}">{{$.Author}}</a>{{else}}{{.Author}}{{end}}{{end}}

templates/pull.html modified

@@ -55,7 +55,7 @@
5555 </div>
5656 {{if .Data.Commits}}
5757 <h3>Commits</h3>
58-<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a>{{with signer .}}<span class="verified" title="Signed with a key registered by ~{{.}}">Verified ~{{.}}</span>{{end}}</div><div class="muted small">{{.Author}} · {{ago .Time}}</div></div>{{end}}</div>
58+<div class="log">{{range .Data.Commits}}<div class="event"><div><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> <a class="subject" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{.Subject}}</a>{{with signer .}}<span class="verified" title="Signed with a key registered by ~{{.}}">Verified ~{{.}}</span>{{end}}</div><div class="muted small">{{template "who" .}} · {{ago .Time}}</div></div>{{end}}</div>
5959 {{end}}
6060 {{if .Data.Files}}
6161 <h3 id="diff">Changes <span class="muted">{{len .Data.Files}} files, <span class="add">+{{.Data.Added}}</span> <span class="del">−{{.Data.Removed}}</span>{{if .Data.ReviewCount}}, {{.Data.ReviewCount}} review comments{{end}}</span></h3>

templates/repo.html modified

@@ -7,7 +7,7 @@ git push -u origin {{$p.Ref}}</pre>
77 {{else}}
88 <div class="summary">
99 <div class="commits">
10-{{range .Data.Commits}}<div class="event"><div class="row"><span><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> — {{.Author}}{{with signer .}}<span class="verified" title="Signed with a key registered by ~{{.}}">Verified</span>{{end}}</span><a class="when" href="/{{$p.Repo.Path}}/commit/{{.Hash}}" title="{{date .Time}}">{{ago .Time}}</a></div><pre class="subject">{{.Subject}}</pre></div>{{end}}
10+{{range .Data.Commits}}<div class="event"><div class="row"><span><a class="hash" href="/{{$p.Repo.Path}}/commit/{{.Hash}}">{{short .Hash}}</a> — {{template "who" .}}{{with signer .}}<span class="verified" title="Signed with a key registered by ~{{.}}">Verified</span>{{end}}</span><a class="when" href="/{{$p.Repo.Path}}/commit/{{.Hash}}" title="{{date .Time}}">{{ago .Time}}</a></div><pre class="subject">{{.Subject}}</pre></div>{{end}}
1111 </div>
1212 <div class="refs">
1313 <h3>refs</h3>

web.go modified

@@ -28,6 +28,7 @@ import (
2828 "github.com/yuin/goldmark"
2929 highlighting "github.com/yuin/goldmark-highlighting/v2"
3030 "github.com/yuin/goldmark/extension"
31+ gmhtml "github.com/yuin/goldmark/renderer/html"
3132 )
3233
3334 //go:embed templates/*.html
@@ -81,7 +82,13 @@ func newServer(store *Store, root, host string, open bool, sshUser string) *Serv
8182 "inc": func(i int) int { return i + 1 },
8283 "perm": perm,
8384 "signer": s.signer,
84- "mark": mark,
85+ "person": func(email string) string {
86+ if u, err := s.store.UserByEmail(email); err == nil {
87+ return u.Name
88+ }
89+ return ""
90+ },
91+ "mark": mark,
8592 "dict": func(kv ...any) map[string]any {
8693 m := map[string]any{}
8794 for i := 0; i+1 < len(kv); i += 2 {
@@ -106,9 +113,13 @@ func newServer(store *Store, root, host string, open bool, sshUser string) *Serv
106113 s.pages[strings.TrimSuffix(base, ".html")] = template.Must(template.New("layout.html").Funcs(funcs).ParseFS(templateFS, "templates/layout.html", "templates/meta.html", name))
107114 }
108115 s.markdown = goldmark.New(goldmark.WithExtensions(extension.GFM, mathjax.MathJax,
109- highlighting.NewHighlighting(highlighting.WithFormatOptions(chromahtml.WithClasses(true)))))
116+ highlighting.NewHighlighting(highlighting.WithGuessLanguage(true), highlighting.WithFormatOptions(chromahtml.WithClasses(true)))),
117+ goldmark.WithRendererOptions(gmhtml.WithUnsafe()))
110118 s.policy = bluemonday.UGCPolicy()
111119 s.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^[a-z]{1,8}( [a-z]{1,8})*$`)).OnElements("span", "pre", "code")
120+ s.policy.AllowElements("video", "source")
121+ s.policy.AllowAttrs("src", "type").OnElements("video", "source")
122+ s.policy.AllowAttrs("controls", "muted", "loop", "playsinline", "poster", "width", "height").OnElements("video")
112123 s.writeSigners()
113124 s.writeKeyring()
114125 s.installHooks()
@@ -215,15 +226,17 @@ func (s *Server) handler() http.Handler {
215226
216227 func (s *Server) index(w http.ResponseWriter, r *http.Request) {
217228 user := s.current(r)
218- recent, err := s.store.RecentRepos(20)
229+ recent, err := s.store.PublicRepos("", "")
219230 if err != nil {
220231 s.fail(w, r, 500, err.Error())
221232 return
222233 }
234+ recent = s.byUpdate(recent)[:min(len(recent), 20)]
223235 var repos, shared []Repo
224236 if user != nil {
225237 repos, _ = s.store.ReposByOwner(user.ID, true)
226238 shared, _ = s.store.SharedRepos(user.ID)
239+ repos, shared = s.byUpdate(repos), s.byUpdate(shared)
227240 }
228241 users, public := s.store.Stats()
229242 s.render(w, r, "index", page{Title: s.host, User: user, Data: map[string]any{"Repos": repos, "Shared": shared, "Recent": recent, "Users": users, "Public": public}})
@@ -446,16 +459,21 @@ func (s *Server) projects(w http.ResponseWriter, r *http.Request) {
446459 s.fail(w, r, 500, err.Error())
447460 return
448461 }
449- for i := range repos {
450- repos[i].Updated = repoUpdated(s.repoDir(repos[i]))
451- }
452462 if sort != "created" {
453- slices.SortStableFunc(repos, func(a, b Repo) int { return b.Updated.Compare(a.Updated) })
463+ repos = s.byUpdate(repos)
454464 }
455465 users, public := s.store.Stats()
456466 s.render(w, r, "projects", page{Title: "Browse public projects", Data: map[string]any{"Repos": repos, "Query": query, "Tag": tag, "Sort": sort, "Users": users, "Public": public}})
457467 }
458468
469+func (s *Server) byUpdate(repos []Repo) []Repo {
470+ for i := range repos {
471+ repos[i].Updated = repoUpdated(s.repoDir(repos[i]))
472+ }
473+ slices.SortStableFunc(repos, func(a, b Repo) int { return b.Updated.Compare(a.Updated) })
474+ return repos
475+}
476+
459477 func repoUpdated(dir string) time.Time {
460478 var latest time.Time
461479 consider := func(p string) {
@@ -727,6 +745,7 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) {
727745 s.fail(w, r, 500, err.Error())
728746 return
729747 }
748+ repos = s.byUpdate(repos)
730749 profile := s.store.Profile(owner.ID)
731750 var bio template.HTML
732751 if profile.Bio != "" {
@@ -1062,6 +1081,14 @@ func (s *Server) tree(w http.ResponseWriter, r *http.Request, repo Repo, spec st
10621081 }
10631082 }
10641083
1084+const maxMedia = 64 << 20
1085+
1086+var mediaTypes = map[string]string{
1087+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".avif": "image/avif",
1088+ ".mp4": "video/mp4", ".webm": "video/webm", ".ogv": "video/ogg", ".mov": "video/quicktime",
1089+ ".mp3": "audio/mpeg", ".ogg": "audio/ogg", ".wav": "audio/wav", ".flac": "audio/flac",
1090+}
1091+
10651092 func (s *Server) raw(w http.ResponseWriter, r *http.Request, repo Repo, spec string) {
10661093 g := Git{Dir: s.repoDir(repo)}
10671094 ref, filePath, ok := s.splitRef(g, spec)
@@ -1083,10 +1110,22 @@ func (s *Server) raw(w http.ResponseWriter, r *http.Request, repo Repo, spec str
10831110 s.fail(w, r, 404, "No such path")
10841111 return
10851112 }
1086- w.Header().Set("Content-Type", "text/plain; charset=utf-8")
10871113 w.Header().Set("X-Content-Type-Options", "nosniff")
1088- w.Header().Set("Content-Length", strconv.FormatInt(g.Size(commit, filePath), 10))
10891114 w.Header().Set("Content-Disposition", mime.FormatMediaType("inline", map[string]string{"filename": filepath.Base(filePath)}))
1115+ size := g.Size(commit, filePath)
1116+ if kind, ok := mediaTypes[strings.ToLower(filepath.Ext(filePath))]; ok && size <= maxMedia {
1117+ data, err := g.Blob(commit, filePath)
1118+ if err != nil {
1119+ s.fail(w, r, 500, err.Error())
1120+ return
1121+ }
1122+ w.Header().Set("Content-Type", kind)
1123+ w.Header().Set("Cache-Control", "public, max-age=300")
1124+ http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(data))
1125+ return
1126+ }
1127+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
1128+ w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
10901129 g.Stream(w, commit, filePath)
10911130 }
10921131