~gitpocket/gitpocket

8

feat(index): link the importer from the dashboard

commit
e76f2d7b32a09f67e3cfecf761e1901ed081aaff browse tree
parent
b0461c8f8942977ade0020d163974ec804692297
author
~james <354576047+[email protected]>
signature
Signed by: ~james SHA256:KlzJ0P0gvJcnOYpaxD5yCUw52ImHttTKRjVAEnnYnKg
date
2026-11-13 17:47 UTC

7 files changed, +94 −12

forge/flow_test.go modified

@@ -14,6 +14,7 @@ import (
1414 "path/filepath"
1515 "strings"
1616 "testing"
17+ "time"
1718 )
1819
1920 func testServer(t *testing.T) (*Server, http.Handler) {
@@ -289,9 +290,11 @@ func TestPins(t *testing.T) {
289290 }
290291 pin(ids[2], own)
291292 pin(ids[0], own)
293+ past := time.Now().Add(-time.Hour)
294+ os.Chtimes(filepath.Join(s.root, "repos", "alice", "c.git"), past, past)
292295 body := get(h, "/~alice", "").Body.String()
293- if !strings.Contains(body, `href="/~alice/c"`) || strings.Index(body, `href="/~alice/c"`) > strings.Index(body, `href="/~alice/a"`) || strings.Contains(body, `href="/~alice/b"`) {
294- t.Error("profile does not show exactly the pinned repos in pin order")
296+ if !strings.Contains(body, `href="/~alice/c"`) || strings.Index(body, `href="/~alice/a"`) > strings.Index(body, `href="/~alice/c"`) || strings.Contains(body, `href="/~alice/b"`) {
297+ t.Error("profile does not show exactly the pinned repos, most recently updated first")
295298 }
296299 for _, id := range ids[1:] {
297300 pin(id, own)

forge/import.go modified

@@ -9,6 +9,7 @@ import (
99 "fmt"
1010 "html/template"
1111 "io"
12+ "io/fs"
1213 "log"
1314 "net"
1415 "net/http"
@@ -16,6 +17,7 @@ import (
1617 "os"
1718 "os/exec"
1819 "path"
20+ "path/filepath"
1921 "slices"
2022 "strconv"
2123 "strings"
@@ -225,6 +227,7 @@ func (s *Server) runImport(repo Repo, src, pin, token string) {
225227 if over, used, quota := overQuota(s.root, repo.Owner.Name); over {
226228 return errors.New(quotaMessage(used, quota))
227229 }
230+ settleRefs(dir)
228231 return nil
229232 }()
230233 if err != nil {
@@ -386,6 +389,7 @@ func (s *Server) claim(w http.ResponseWriter, r *http.Request, repo Repo) {
386389 http.Redirect(w, r, "/"+repo.Path()+"/claim?error="+template.URLQueryEscaper("Could not rewrite the history"), http.StatusSeeOther)
387390 return
388391 }
392+ settleRefs(s.repoDir(repo))
389393 }
390394 http.Redirect(w, r, "/"+repo.Path(), http.StatusSeeOther)
391395 return
@@ -394,6 +398,27 @@ func (s *Server) claim(w http.ResponseWriter, r *http.Request, repo Repo) {
394398 s.render(w, r, "claim", page{Title: "Claim commits", Error: r.URL.Query().Get("error"), Data: map[string]any{"Page": rp, "Identities": ids, "Name": name, "Email": email}})
395399 }
396400
401+func settleRefs(dir string) {
402+ out, err := Git{Dir: dir}.run(nil, "for-each-ref", "--sort=-creatordate", "--count=1", "--format=%(creatordate:unix)", "refs/heads", "refs/tags")
403+ if err != nil {
404+ return
405+ }
406+ unix, err := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64)
407+ if err != nil {
408+ return
409+ }
410+ stamp := time.Unix(unix, 0)
411+ os.Chtimes(filepath.Join(dir, "packed-refs"), stamp, stamp)
412+ for _, sub := range []string{"heads", "tags"} {
413+ filepath.WalkDir(filepath.Join(dir, "refs", sub), func(p string, d fs.DirEntry, err error) error {
414+ if err == nil && !d.IsDir() {
415+ os.Chtimes(p, stamp, stamp)
416+ }
417+ return nil
418+ })
419+ }
420+}
421+
397422 var signedCommitsFlag = sync.OnceValue(func() bool {
398423 out, _ := exec.Command("git", "fast-export", "-h").CombinedOutput()
399424 return bytes.Contains(out, []byte("signed-commits"))

forge/unit_test.go modified

@@ -9,6 +9,7 @@ import (
99 "strconv"
1010 "strings"
1111 "testing"
12+ "time"
1213 )
1314
1415 func TestValidBranch(t *testing.T) {
@@ -192,3 +193,29 @@ func TestRewriteAuthors(t *testing.T) {
192193 t.Error("tag not moved to the rewritten commit")
193194 }
194195 }
196+
197+func TestSettleRefs(t *testing.T) {
198+ dir := t.TempDir()
199+ when := "2020-03-04T05:06:07Z"
200+ for _, args := range [][]string{
201+ {"init", "-q", "-b", "main"},
202+ {"-c", "user.name=a", "-c", "user.email=a@b", "commit", "-q", "--allow-empty", "-m", "old"},
203+ {"tag", "v1"},
204+ {"pack-refs", "--all"},
205+ {"branch", "feat/x"},
206+ } {
207+ cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
208+ cmd.Env = append(os.Environ(), "GIT_AUTHOR_DATE="+when, "GIT_COMMITTER_DATE="+when)
209+ if out, err := cmd.CombinedOutput(); err != nil {
210+ t.Fatalf("%v: %s", err, out)
211+ }
212+ }
213+ gitDir := filepath.Join(dir, ".git")
214+ if repoUpdated(gitDir).Before(time.Now().Add(-time.Hour)) {
215+ t.Fatal("fresh refs already look old")
216+ }
217+ settleRefs(gitDir)
218+ if got := repoUpdated(gitDir).UTC().Format(time.RFC3339); got != when {
219+ t.Errorf("updated %s, want %s", got, when)
220+ }
221+}

forge/web.go modified

@@ -147,6 +147,7 @@ func newServer(store *Store, root, host, name string, sshUser string) *Server {
147147 s.policy.AllowElements("video", "source")
148148 s.policy.AllowAttrs("src", "type").OnElements("video", "source")
149149 s.policy.AllowAttrs("controls", "muted", "loop", "playsinline", "poster", "width", "height").OnElements("video")
150+ s.policy.AllowAttrs("align").Matching(regexp.MustCompile(`^(center|left|right)$`)).OnElements("p", "div", "h1", "h2", "h3", "h4")
150151 s.writeSigners()
151152 s.writeKeyring()
152153 s.installHooks()
@@ -588,10 +589,12 @@ func repoUpdated(dir string) time.Time {
588589 }
589590 consider(filepath.Join(dir, "packed-refs"))
590591 for _, sub := range []string{"heads", "tags"} {
591- entries, _ := os.ReadDir(filepath.Join(dir, "refs", sub))
592- for _, e := range entries {
593- consider(filepath.Join(dir, "refs", sub, e.Name()))
594- }
592+ filepath.WalkDir(filepath.Join(dir, "refs", sub), func(p string, d fs.DirEntry, err error) error {
593+ if err == nil && !d.IsDir() {
594+ consider(p)
595+ }
596+ return nil
597+ })
595598 }
596599 if latest.IsZero() {
597600 consider(dir)
@@ -1188,7 +1191,18 @@ func (s *Server) renderMarkdown(src []byte) template.HTML {
11881191 return s.renderMarkdownAt(src, "", "")
11891192 }
11901193
1191-var relLink = regexp.MustCompile(`(src|href)="([^"]+)"`)
1194+var (
1195+ relLink = regexp.MustCompile(`(src|href|poster)="([^"]+)"`)
1196+ posterLink = regexp.MustCompile(`<a href="([^"]+\.(?i:mp4|webm|ogv|mov))"[^>]*>\s*<img src="([^"]+)"[^>]*>\s*</a>`)
1197+ videoImg = regexp.MustCompile(`<img src="([^"]+\.(?i:mp4|webm|ogv|mov))"[^>]*>`)
1198+ videoLine = regexp.MustCompile(`<p>\s*<a href="([^"]+\.(?i:mp4|webm|ogv|mov))"[^>]*>[^<]*</a>\s*</p>`)
1199+)
1200+
1201+func embedVideos(out []byte) []byte {
1202+ out = posterLink.ReplaceAll(out, []byte(`<video controls playsinline preload="metadata" src="$1" poster="$2"></video>`))
1203+ out = videoImg.ReplaceAll(out, []byte(`<video controls playsinline preload="metadata" src="$1"></video>`))
1204+ return videoLine.ReplaceAll(out, []byte(`<p><video controls playsinline preload="metadata" src="$1"></video></p>`))
1205+}
11921206
11931207 func (s *Server) renderMarkdownAt(src []byte, rawBase, treeBase string) template.HTML {
11941208 src = displayMath.ReplaceAll(src, []byte("$1$$$$\n$2\n$$$$"))
@@ -1196,7 +1210,7 @@ func (s *Server) renderMarkdownAt(src []byte, rawBase, treeBase string) template
11961210 if err := s.markdown.Convert(src, &buf); err != nil {
11971211 return template.HTML("<pre>" + template.HTMLEscapeString(string(src)) + "</pre>")
11981212 }
1199- out := s.policy.SanitizeBytes(buf.Bytes())
1213+ out := embedVideos(s.policy.SanitizeBytes(buf.Bytes()))
12001214 if rawBase != "" {
12011215 out = relLink.ReplaceAllFunc(out, func(m []byte) []byte {
12021216 parts := relLink.FindSubmatch(m)
@@ -1206,7 +1220,7 @@ func (s *Server) renderMarkdownAt(src []byte, rawBase, treeBase string) template
12061220 }
12071221 target = strings.TrimPrefix(target, "./")
12081222 base := treeBase
1209- if attr == "src" {
1223+ if attr != "href" {
12101224 base = rawBase
12111225 }
12121226 return []byte(attr + `="` + html.EscapeString(escapePath(base)) + target + `"`)
@@ -1286,6 +1300,12 @@ func (s *Server) tree(w http.ResponseWriter, r *http.Request, repo Repo, spec st
12861300 case "blob":
12871301 size := g.Size(rp.Commit, filePath)
12881302 data["Size"] = size
1303+ if kind, ok := mediaTypes[strings.ToLower(filepath.Ext(filePath))]; ok && size <= maxMedia {
1304+ data["File"] = true
1305+ data["Media"], _, _ = strings.Cut(kind, "/")
1306+ s.render(w, r, "blob", page{Title: repo.Path() + "/" + filePath, Data: data})
1307+ return
1308+ }
12891309 var content []byte
12901310 if size <= maxRender {
12911311 if content, err = g.Blob(rp.Commit, filePath); err != nil {
@@ -1720,7 +1740,6 @@ func pinnedRepos(repos []Repo, pins map[int64]int) []Repo {
17201740 out = append(out, r)
17211741 }
17221742 }
1723- slices.SortFunc(out, func(a, b Repo) int { return pins[a.ID] - pins[b.ID] })
17241743 return out
17251744 }
17261745

web/static/style.css modified

@@ -333,6 +333,13 @@ h3.featured{font-size:1.3rem;font-weight:400;margin:2rem 0 .5rem}
333333 .landing .more .btn{min-width:20rem;text-align:center}
334334 @media (max-width:700px){.featuredgrid{grid-template-columns:1fr}.landing .more .btn{min-width:0;width:100%}}
335335 .readme video,.readme img{max-width:100%;height:auto}
336+.readme video{display:block;margin:.6rem auto;background:#000}
337+.readme [align=center]{text-align:center}
338+.readme [align=right]{text-align:right}
339+.media{display:flex;justify-content:center;padding:1rem 0}
340+.media video,.media img{max-width:100%;max-height:80vh;height:auto}
341+.media video{background:#000}
342+.media audio{width:min(100%,40rem)}
336343 .auth h3{white-space:nowrap}
337344 .pickaxe{display:flex;gap:.5rem;margin:0 0 1rem;flex-wrap:wrap}.pickaxe input{flex:1;min-width:14rem}
338345 .pickaxe select{font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--edge);padding:.3rem .4rem}

web/templates/blob.tmpl modified

@@ -1,7 +1,8 @@
11 {{define "content"}}
22 {{$p := .Data.Page}}
33 {{template "filebar" .Data}}
4-{{if .Data.Binary}}<p class="muted">Binary file, <a href="/{{$p.Repo.Path}}/raw/{{$p.Ref}}/{{.Data.Path}}">view raw</a>.</p>
4+{{if .Data.Media}}{{$src := printf "/%s/raw/%s/%s" $p.Repo.Path $p.Ref .Data.Path}}<div class="media">{{if eq .Data.Media "video"}}<video controls playsinline preload="metadata" src="{{$src}}"></video>{{else if eq .Data.Media "audio"}}<audio controls preload="metadata" src="{{$src}}"></audio>{{else}}<img src="{{$src}}" alt="{{.Data.Path}}">{{end}}</div>
5+{{else if .Data.Binary}}<p class="muted">Binary file, <a href="/{{$p.Repo.Path}}/raw/{{$p.Ref}}/{{.Data.Path}}">view raw</a>.</p>
56 {{else if .Data.Large}}<p class="muted">File too large to display, <a href="/{{$p.Repo.Path}}/raw/{{$p.Ref}}/{{.Data.Path}}">view raw</a>.</p>
67 {{else if .Data.Markdown}}<p class="muted small"><a href="?source=1">View source</a></p><article class="readme">{{.Data.Markdown}}</article>
78 {{else if .Data.Highlighted}}<div class="code">{{.Data.Highlighted}}</div>

web/templates/index.tmpl modified

@@ -1,7 +1,7 @@
11 {{define "content"}}
22 {{if .User}}
33 <div class="dash">
4-<section><div class="dashhead"><h3>Your repositories</h3><span><a class="btn" href="/orgs/new">New organization</a> <a class="btn primary" href="/create">New repository</a></span></div>
4+<section><div class="dashhead"><h3>Your repositories</h3><span><a class="btn" href="/orgs/new">New organization</a> <a class="btn" href="/import">Import repository</a> <a class="btn primary" href="/create">New repository</a></span></div>
55 {{if .Data.Repos}}{{template "repolist" .Data.Repos}}{{else}}<p class="muted">You have no repositories yet.</p>{{end}}
66 {{if .Data.Shared}}<h3>Shared with you</h3>{{template "repolist" .Data.Shared}}{{end}}</section>
77 {{else}}