feat(gists): title gists by file, show descriptions as text and list private ones to their owner
9 files changed, +27 −11
- forge/gist.go modified +8 −0
- forge/gist_test.go modified +5 −3
- forge/gistweb.go modified +6 −2
- web/static/style.css modified +1 −0
- web/templates/admin.tmpl modified +1 −1
- web/templates/gist.tmpl modified +3 −2
- web/templates/gists.tmpl modified +2 −1
- web/templates/layout.tmpl modified +0 −1
- web/templates/search.tmpl modified +1 −1
forge/gist.go modified
| @@ -114,6 +114,10 @@ func (s *Store) Gists(owner int64, withSecret bool, limit int) ([]Gist, error) { | ||
| 114 | 114 | 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)) |
| 115 | 115 | } |
| 116 | 116 | |
| 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 | + | |
| 117 | 121 | func (s *Store) GistForks(id string) ([]Gist, error) { |
| 118 | 122 | return scanGists(s.db.Query(`select `+gistColumns+` and g.parent = ? and g.secret = 0 order by g.created desc limit 100`, id)) |
| 119 | 123 | } |
| @@ -571,6 +575,10 @@ func (g Gist) Title(files []string) string { | ||
| 571 | 575 | if g.Description != "" { |
| 572 | 576 | return g.Description |
| 573 | 577 | } |
| 578 | + return g.Name(files) | |
| 579 | +} | |
| 580 | + | |
| 581 | +func (g Gist) Name(files []string) string { | |
| 574 | 582 | if len(files) > 0 { |
| 575 | 583 | return files[0] |
| 576 | 584 | } |
forge/gist_test.go modified
| @@ -68,13 +68,15 @@ func TestGists(t *testing.T) { | ||
| 68 | 68 | } |
| 69 | 69 | |
| 70 | 70 | 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} { | |
| 72 | 72 | if strings.Contains(get(h, path, cookie).Body.String(), secret) { |
| 73 | 73 | t.Errorf("%s lists a secret gist", path) |
| 74 | 74 | } |
| 75 | 75 | } |
| 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 | + } | |
| 78 | 80 | } |
| 79 | 81 | |
| 80 | 82 | 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}$`) | ||
| 20 | 20 | |
| 21 | 21 | func (s *Server) gistIndex(w http.ResponseWriter, r *http.Request) { |
| 22 | 22 | 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) | |
| 24 | 28 | if err != nil { |
| 25 | 29 | s.fail(w, r, 500, err.Error()) |
| 26 | 30 | return |
| @@ -183,7 +187,7 @@ func (s *Server) gistPage(w http.ResponseWriter, r *http.Request, gist Gist, use | ||
| 183 | 187 | math = math || hasMath(v.Markdown) |
| 184 | 188 | } |
| 185 | 189 | 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, | |
| 187 | 191 | "Comments": rendered, "Owner": user != nil && user.ID == gist.Owner.ID, |
| 188 | 192 | "Admin": user != nil && s.store.IsAdmin(user.ID), "Starred": user != nil && s.store.GistStarred(user.ID, gist.ID), |
| 189 | 193 | "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 | ||
| 561 | 561 | pre.reason{white-space:pre-wrap;word-break:break-word} |
| 562 | 562 | .gisthead{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:.8rem;margin-bottom:.6rem} |
| 563 | 563 | .gisthead h2{font-size:1.3rem;overflow-wrap:anywhere} |
| 564 | +.gistdesc{margin:.15rem 0 .35rem;overflow-wrap:anywhere} | |
| 564 | 565 | .gistactions{display:flex;flex-wrap:wrap;gap:.4rem} |
| 565 | 566 | .gistactions form.inline{margin:0} |
| 566 | 567 | nav.gisttabs,nav.gistfilter{border-bottom:1px solid var(--bar);margin-bottom:.8rem} |
web/templates/admin.tmpl modified
| @@ -68,7 +68,7 @@ | ||
| 68 | 68 | <h3 class="rule">Gists</h3> |
| 69 | 69 | <p class="muted">Every gist, secret ones included, reported ones first and then the largest. Size counts the repository on disk.</p> |
| 70 | 70 | <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> | |
| 72 | 72 | <td><a href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a></td> |
| 73 | 73 | <td class="muted">{{if .Secret}}secret{{else}}public{{end}}</td> |
| 74 | 74 | <td class="muted">{{.Files}} file{{if ne .Files 1}}s{{end}}</td> |
web/templates/gist.tmpl modified
| @@ -3,9 +3,10 @@ | ||
| 3 | 3 | {{$g := $d.Gist}} |
| 4 | 4 | <div class="gisthead"> |
| 5 | 5 | <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}} | |
| 7 | 8 | <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&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}} | |
| 9 | 10 | </div> |
| 10 | 11 | <div class="gistactions"> |
| 11 | 12 | {{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 @@ | ||
| 8 | 8 | <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> |
| 9 | 9 | {{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}} |
| 10 | 10 | {{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}} | |
| 12 | 13 | <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> |
| 13 | 14 | {{range .Preview}}<a class="gistpreview" href="/gist/{{$g.ID}}" aria-label="{{.Name}}">{{template "gistbody" .}}</a>{{end}} |
| 14 | 15 | </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 @@ | ||
| 21 | 21 | <nav class="site"> |
| 22 | 22 | <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> |
| 23 | 23 | <a href="/search">explore</a> |
| 24 | -<a href="/gists">gists</a> | |
| 25 | 24 | {{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}} |
| 26 | 25 | <span class="spacer"></span> |
| 27 | 26 | {{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 @@ | ||
| 25 | 25 | {{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}} |
| 26 | 26 | {{else if eq $d.Type "gists"}} |
| 27 | 27 | <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}} | |
| 29 | 29 | {{else}} |
| 30 | 30 | <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> |
| 31 | 31 | {{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}} |