~gitpocket/gitpocket

8

feat(gists): title gists by file, show descriptions as text and list private ones to their owner

commit
57b664da5a36ffb9fba54f87852992050d5ca63b browse tree
parent
161949a0fab0794bbb7e894db46ad9ede8bc5ce2
author
~james <354576047+[email protected]>
signature
Signed by: ~james SHA256:KlzJ0P0gvJcnOYpaxD5yCUw52ImHttTKRjVAEnnYnKg
date
2026-11-14 17:06 UTC

9 files changed, +27 −11

forge/gist.go modified

@@ -114,6 +114,10 @@ func (s *Store) Gists(owner int64, withSecret bool, limit int) ([]Gist, error) {
114114 return scanGists(s.db.Query(`select `+gistColumns+` and (? = 0 or g.owner = ?) and (g.secret = 0 or ?) order by g.updated desc limit ?`, owner, owner, withSecret, limit))
115115 }
116116
117+func (s *Store) GistFeed(viewer int64, limit int) ([]Gist, error) {
118+ return scanGists(s.db.Query(`select `+gistColumns+` and (g.secret = 0 or g.owner = ?) order by g.updated desc limit ?`, viewer, limit))
119+}
120+
117121 func (s *Store) GistForks(id string) ([]Gist, error) {
118122 return scanGists(s.db.Query(`select `+gistColumns+` and g.parent = ? and g.secret = 0 order by g.created desc limit 100`, id))
119123 }
@@ -571,6 +575,10 @@ func (g Gist) Title(files []string) string {
571575 if g.Description != "" {
572576 return g.Description
573577 }
578+ return g.Name(files)
579+}
580+
581+func (g Gist) Name(files []string) string {
574582 if len(files) > 0 {
575583 return files[0]
576584 }

forge/gist_test.go modified

@@ -68,13 +68,15 @@ func TestGists(t *testing.T) {
6868 }
6969
7070 secret := newGist(t, h, ac, url.Values{"name": {"key.txt"}, "content": {"hidden"}, "orig": {""}, "visibility": {"secret"}})
71- for path, cookie := range map[string]string{"/gists": ac, "/~alice/gists": bc} {
71+ for path, cookie := range map[string]string{"/gists": bc, "/~alice/gists": bc} {
7272 if strings.Contains(get(h, path, cookie).Body.String(), secret) {
7373 t.Errorf("%s lists a secret gist", path)
7474 }
7575 }
76- if !strings.Contains(get(h, "/~alice/gists", ac).Body.String(), secret) {
77- t.Error("owner cannot see their secret gist")
76+ for _, path := range []string{"/gists", "/~alice/gists"} {
77+ if body := get(h, path, ac).Body.String(); !strings.Contains(body, secret) || !strings.Contains(body, `<span class="tag">private</span>`) {
78+ t.Errorf("%s hides the owner's private gist", path)
79+ }
7880 }
7981
8082 for _, path := range []string{"/gists/new", "/gist/" + id + "/edit", "/gists", "/~alice/gists?tab=starred"} {

forge/gistweb.go modified

@@ -20,7 +20,11 @@ var fullOID = regexp.MustCompile(`^[0-9a-f]{40}$`)
2020
2121 func (s *Server) gistIndex(w http.ResponseWriter, r *http.Request) {
2222 user := s.current(r)
23- gists, err := s.store.Gists(0, false, 50)
23+ var viewer int64
24+ if user != nil {
25+ viewer = user.ID
26+ }
27+ gists, err := s.store.GistFeed(viewer, 50)
2428 if err != nil {
2529 s.fail(w, r, 500, err.Error())
2630 return
@@ -183,7 +187,7 @@ func (s *Server) gistPage(w http.ResponseWriter, r *http.Request, gist Gist, use
183187 math = math || hasMath(v.Markdown)
184188 }
185189 data := map[string]any{
186- "Gist": gist, "Files": views, "Title": gist.Title(files), "Commit": commit, "Head": head, "Old": commit != head,
190+ "Gist": gist, "Files": views, "Name": gist.Name(files), "Commit": commit, "Head": head, "Old": commit != head,
187191 "Comments": rendered, "Owner": user != nil && user.ID == gist.Owner.ID,
188192 "Admin": user != nil && s.store.IsAdmin(user.ID), "Starred": user != nil && s.store.GistStarred(user.ID, gist.ID),
189193 "Clone": "https://" + s.host + "/gist/" + gist.ID + ".git", "SSH": s.sshUser + "@" + s.sshHost + ":gist/" + gist.ID + ".git",

web/static/style.css modified

@@ -561,6 +561,7 @@ button.link{background:none;border:0;padding:0;color:inherit;font:inherit;cursor
561561 pre.reason{white-space:pre-wrap;word-break:break-word}
562562 .gisthead{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:.8rem;margin-bottom:.6rem}
563563 .gisthead h2{font-size:1.3rem;overflow-wrap:anywhere}
564+.gistdesc{margin:.15rem 0 .35rem;overflow-wrap:anywhere}
564565 .gistactions{display:flex;flex-wrap:wrap;gap:.4rem}
565566 .gistactions form.inline{margin:0}
566567 nav.gisttabs,nav.gistfilter{border-bottom:1px solid var(--bar);margin-bottom:.8rem}

web/templates/admin.tmpl modified

@@ -68,7 +68,7 @@
6868 <h3 class="rule">Gists</h3>
6969 <p class="muted">Every gist, secret ones included, reported ones first and then the largest. Size counts the repository on disk.</p>
7070 <table class="keys">{{range $d.Gists}}<tr>
71-<td><a href="/gist/{{.ID}}">{{if .Description}}{{.Description}}{{else}}{{short .ID}}{{end}}</a></td>
71+<td><a href="/gist/{{.ID}}">{{short .ID}}</a>{{with .Description}} <span class="muted">{{.}}</span>{{end}}</td>
7272 <td><a href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a></td>
7373 <td class="muted">{{if .Secret}}secret{{else}}public{{end}}</td>
7474 <td class="muted">{{.Files}} file{{if ne .Files 1}}s{{end}}</td>

web/templates/gist.tmpl modified

@@ -3,9 +3,10 @@
33 {{$g := $d.Gist}}
44 <div class="gisthead">
55 <div>
6-<h2><a href="/~{{$g.Owner.Name}}">~{{$g.Owner.Name}}</a> / <a href="/gist/{{$g.ID}}">{{$d.Title}}</a>{{if $g.Secret}} <span class="tag">secret</span>{{end}}</h2>
6+<h2><a href="/~{{$g.Owner.Name}}">~{{$g.Owner.Name}}</a> / <a href="/gist/{{$g.ID}}">{{$d.Name}}</a>{{if $g.Secret}} <span class="tag">private</span>{{end}}</h2>
7+{{with $g.Description}}<p class="gistdesc">{{.}}</p>{{end}}
78 <p class="muted small">{{with $d.Last}}{{template "who" .}} · {{end}}created {{ago $g.Created}}{{if ne $g.Updated $g.Created}} · updated {{ago $g.Updated}}{{end}}{{with $d.Parent}} · forked from <a href="/gist/{{.ID}}">~{{.Owner.Name}}/{{.Title nil}}</a>{{end}}{{if .User}} · <a class="muted" href="/report?kind=gist&amp;target={{$g.ID}}">report</a>{{end}}</p>
8-{{if $g.Secret}}<p class="muted small">Secret gists are unlisted, anyone with this link can read them.</p>{{end}}
9+{{if $g.Secret}}<p class="muted small">Private gists are unlisted, only you see them in lists but anyone with this link can read them.</p>{{end}}
910 </div>
1011 <div class="gistactions">
1112 {{if .User}}<form method="post" action="/gist/{{$g.ID}}/star" class="inline"><button class="btn{{if $d.Starred}} starred{{end}}">{{if $d.Starred}}Unstar{{else}}Star{{end}} <span class="count">{{$g.Stars}}</span></button></form>

web/templates/gists.tmpl modified

@@ -8,7 +8,8 @@
88 <nav class="tabs small gistfilter"><a href="/~{{.Name}}/gists"{{if not $d.Starred}} class="active"{{end}}>all</a><a href="/~{{.Name}}/gists?tab=starred"{{if $d.Starred}} class="active"{{end}}>starred</a></nav>
99 {{else}}<div class="userhead"><h2>Gists</h2><p class="muted">Share snippets, notes and single files. Every gist is a git repository you can clone, fork and push to.</p>{{if $.User}}<a class="btn primary newrepo" href="/gists/new">New gist</a>{{end}}</div>{{end}}
1010 {{if $d.Items}}<div class="events gists">{{range $d.Items}}{{$g := .Gist}}<div class="event gistitem">
11-<h4><a href="/~{{$g.Owner.Name}}">~{{$g.Owner.Name}}</a> / <a href="/gist/{{$g.ID}}">{{$g.Title .Files}}</a>{{if $g.Secret}} <span class="tag">secret</span>{{end}}</h4>
11+<h4><a href="/~{{$g.Owner.Name}}">~{{$g.Owner.Name}}</a> / <a href="/gist/{{$g.ID}}">{{$g.Name .Files}}</a>{{if $g.Secret}} <span class="tag">private</span>{{end}}</h4>
12+{{with $g.Description}}<p class="gistdesc">{{.}}</p>{{end}}
1213 <p class="muted small">{{len .Files}} file{{if ne (len .Files) 1}}s{{end}} · updated {{ago $g.Updated}}{{if $g.Stars}} · {{$g.Stars}} star{{if ne $g.Stars 1}}s{{end}}{{end}}{{if $g.Forks}} · {{$g.Forks}} fork{{if ne $g.Forks 1}}s{{end}}{{end}}{{if $g.Comments}} · {{$g.Comments}} comment{{if ne $g.Comments 1}}s{{end}}{{end}}</p>
1314 {{range .Preview}}<a class="gistpreview" href="/gist/{{$g.ID}}" aria-label="{{.Name}}">{{template "gistbody" .}}</a>{{end}}
1415 </div>{{end}}</div>{{else}}<p class="info">{{if $d.Starred}}No starred gists yet.{{else}}No gists yet.{{end}}</p>{{end}}

web/templates/layout.tmpl modified

@@ -21,7 +21,6 @@
2121 <nav class="site">
2222 <a class="brand" href="/" aria-label="{{.Name}}">{{if .Logo}}<img src="/static/site/logo.png?v={{.Version}}" alt="{{.Name}}">{{else if eq .Name "gitpocket"}}<img class="solo" src="/static/logo.png?v={{.Version}}" alt="gitpocket" width="35" height="36">{{else}}<img class="mark" src="/static/logo.png?v={{$.Version}}" alt="" width="35" height="36">{{.Name}}{{end}}</a>
2323 <a href="/search">explore</a>
24-<a href="/gists">gists</a>
2524 {{if not (and .Data (index .Data "Facets"))}}<form class="navsearch" method="get" action="/search" role="search"><input name="q" placeholder="Search repositories, people, languages" aria-label="Search"></form>{{end}}
2625 <span class="spacer"></span>
2726 {{if .User}}<span class="me"><details class="bell{{if .Unread}} on{{end}}"><summary aria-label="Notifications{{if .Unread}}, {{.Unread}} unread{{end}}" title="Notifications"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.8a4 4 0 0 0-4 4v2.6L2.6 11h10.8L12 8.4V5.8a4 4 0 0 0-4-4zM6.4 13a1.6 1.6 0 0 0 3.2 0"/></svg>{{if .Unread}}<span class="badge">{{if gt .Unread 99}}99+{{else}}{{.Unread}}{{end}}</span>{{end}}</summary><div class="inboxpanel"><input type="radio" name="nview" id="nall" checked><input type="radio" name="nview" id="nunread"{{if .Unread}} checked{{end}}><input type="radio" name="nkind" id="kall" checked><input type="radio" name="nkind" id="kpull"><input type="radio" name="nkind" id="kissue"><div class="seg"><span><label for="nall">Inbox</label><label for="nunread">Unread</label></span><span><label for="kall">All</label><label for="kpull">Pulls</label><label for="kissue">Issues</label></span></div><ul>{{range .Inbox}}<li class="{{.Kind}}{{if .Unread}} unread{{end}}"><a href="/notifications/{{.ID}}"><svg class="kind" viewBox="0 0 16 16" aria-hidden="true">{{if eq .Kind "pull"}}<circle cx="4" cy="3.5" r="1.6"/><circle cx="4" cy="12.5" r="1.6"/><circle cx="12" cy="12.5" r="1.6"/><path d="M4 5.1v5.8M12 10.9V7a2.5 2.5 0 0 0-2.5-2.5H7m1.5-2L7 4.5l1.5 2"/>{{else}}<circle cx="8" cy="8" r="6"/><circle cx="8" cy="8" r="1.2"/>{{end}}</svg><span><span class="where">{{.Repo.Path}} #{{.Number}}</span><span class="title">{{.Title}}</span><span class="who">~{{.Actor}} {{.Verb}} this · {{ago .Created}}</span></span></a></li>{{end}}</ul><div class="empty"><svg class="tray" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 13l2.5-7.5A2 2 0 0 1 7.4 4h9.2a2 2 0 0 1 1.9 1.5L21 13v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM3 13h5l1.5 2.5h5L16 13h5"/></svg><p>All caught up</p></div><a class="viewall" href="/notifications">View all ▸</a></div></details>{{if .Admin}}<a href="/admin">admin</a>{{end}}<span>Logged in as <a href="/~{{.User.Name}}">~{{.User.Name}}</a></span><form method="post" action="/logout"><button>Log out</button></form></span>{{else}}<span class="me auth"><a href="/login">Log in</a>{{if ne .Registration "closed"}}<a href="/register">Register</a>{{end}}</span>{{end}}

web/templates/search.tmpl modified

@@ -25,7 +25,7 @@
2525 {{range $d.Repos}}<div class="hit"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a></h4>{{with .Description}}<p>{{.}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a>{{end}}</p>{{end}}<p class="meta">{{with .Language}}<span><i class="dot" style="background:{{.Colour}}"></i>{{.Name}}</span>{{end}}{{if .Stars}}<span>☆ {{.Stars}}</span>{{end}}<span>Updated {{ago .Updated}}</span></p></div>{{else}}<p class="info">No repositories match. Try a shorter query or clear the language filter.</p>{{end}}
2626 {{else if eq $d.Type "gists"}}
2727 <div class="resulthead"><h3 class="plain">{{len $d.Gists}} gist{{if ne (len $d.Gists) 1}}s{{end}}{{with $d.Query}} matching <q>{{.}}</q>{{end}}</h3></div>
28-{{range $d.Gists}}<div class="hit"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/gist/{{.ID}}">{{if .Description}}{{.Description}}{{else}}{{index .Files 0}}{{end}}</a></h4><p class="tags">{{range .Files}}<code>{{.}}</code> {{end}}</p><p class="meta">{{if .Stars}}<span>☆ {{.Stars}}</span>{{end}}<span>Updated {{ago .Updated}}</span></p></div>{{else}}<p class="info">No public gists match.</p>{{end}}
28+{{range $d.Gists}}<div class="hit"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/gist/{{.ID}}">{{.Name .Files}}</a></h4>{{with .Description}}<p class="bio">{{.}}</p>{{end}}<p class="tags">{{range .Files}}<code>{{.}}</code> {{end}}</p><p class="meta">{{if .Stars}}<span>☆ {{.Stars}}</span>{{end}}<span>Updated {{ago .Updated}}</span></p></div>{{else}}<p class="info">No public gists match.</p>{{end}}
2929 {{else}}
3030 <div class="resulthead"><h3 class="plain">{{len $d.Users}} user{{if ne (len $d.Users) 1}}s{{end}}{{with $d.Query}} matching <q>{{.}}</q>{{end}}{{with $d.Lang}} writing {{.}}{{end}}</h3></div>
3131 {{range $d.Users}}<div class="hit user{{if .Org}} org{{end}}"><a href="/~{{.Name}}" class="hitavatar"><img src="/~{{.Name}}/avatar?v={{.Profile.Avatar}}" alt=""></a><div><h4><a href="/~{{.Name}}">{{with .Profile.Display}}{{.}}{{else}}~{{.Name}}{{end}}</a> <span class="muted">~{{.Name}}</span></h4>{{with .Profile.Bio}}<p class="bio">{{.}}</p>{{end}}<p class="meta">{{with .Profile.Location}}<span>{{.}}</span>{{end}}<span>{{.Repos}} public repositor{{if eq .Repos 1}}y{{else}}ies{{end}}</span><span>{{if .Org}}Since{{else}}Joined{{end}} {{.Created.Format "January 2006"}}</span></p></div></div>{{else}}<p class="info">No users match.</p>{{end}}