~gitpocket/gitpocket

1

feat(web): order repos by last push and serve media

commit
3ff63a0c43f8fe21cd83d886a933a5c7aae93a4b browse tree
parent
727831e8d9908b1e7550e2a72caf5f2f13e4140a
author
obscurejames <354576047+obscurejames@users.noreply.github.com>
signature
Signed by: ~james SHA256:KlzJ0P0gvJcnOYpaxD5yCUw52ImHttTKRjVAEnnYnKg
date
2026-10-03 18:51 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