+178
-32
Diff
round #0
+99
-3
store/store.go
+99
-3
store/store.go
···
10
10
"encoding/json"
11
11
"fmt"
12
12
"io"
13
+
"sort"
13
14
"time"
14
15
15
16
"entgo.io/ent/dialect"
···
395
396
return out, rows.Err()
396
397
}
397
398
398
-
// RecentRepos returns repos created since `since`, optionally filtered by
399
-
// primary language. Pass limit <= 0 for no limit. Uses raw SQL for the
400
-
// scalar star-count subquery and handle JOIN — both awkward in ent.
399
+
// PrimaryLanguages returns the sorted set of "primary" languages observed
400
+
// across all enriched repos — i.e., the highest-bytes language per repo,
401
+
// deduplicated. Used to populate the language filter dropdown.
402
+
func (s *Store) PrimaryLanguages(ctx context.Context) ([]string, error) {
403
+
rows, err := s.db.QueryContext(ctx, `SELECT languages FROM repos WHERE languages IS NOT NULL AND languages != '' AND languages != '{}'`)
404
+
if err != nil {
405
+
return nil, err
406
+
}
407
+
defer rows.Close()
408
+
seen := make(map[string]struct{})
409
+
for rows.Next() {
410
+
var raw string
411
+
if err := rows.Scan(&raw); err != nil {
412
+
return nil, err
413
+
}
414
+
var langs map[string]int64
415
+
if err := json.Unmarshal([]byte(raw), &langs); err != nil {
416
+
continue
417
+
}
418
+
var best string
419
+
var bestN int64
420
+
for k, v := range langs {
421
+
if v > bestN {
422
+
bestN, best = v, k
423
+
}
424
+
}
425
+
if best != "" {
426
+
seen[best] = struct{}{}
427
+
}
428
+
}
429
+
if err := rows.Err(); err != nil {
430
+
return nil, err
431
+
}
432
+
out := make([]string, 0, len(seen))
433
+
for k := range seen {
434
+
out = append(out, k)
435
+
}
436
+
sort.Strings(out)
437
+
return out, nil
438
+
}
439
+
440
+
// ReposFilter narrows the result of RecentRepos. Zero values mean "no
441
+
// filter" so callers can mix and match.
442
+
type ReposFilter struct {
443
+
Language string // primary language match (post-filter in Go over JSON map)
444
+
Since time.Time // repos created at or after this time (zero = no time floor)
445
+
ForksOnly bool // only rows with non-empty source
446
+
NoForks bool // exclude rows with non-empty source
447
+
}
448
+
449
+
// RecentRepos returns repos matching the filter, sorted newest first. Pass
450
+
// limit <= 0 for no limit. Uses raw SQL because the scalar star-count
451
+
// subquery and handle LEFT JOIN are awkward to express via ent's query API.
452
+
func (s *Store) RecentRepos(ctx context.Context, f ReposFilter, limit int) ([]Repo, error) {
453
+
candidateLimit := -1
454
+
if limit > 0 {
455
+
candidateLimit = limit
456
+
if f.Language != "" {
457
+
candidateLimit = limit * 5
458
+
}
459
+
}
460
+
forksOnly := 0
461
+
if f.ForksOnly {
462
+
forksOnly = 1
463
+
}
464
+
noForks := 0
465
+
if f.NoForks {
466
+
noForks = 1
467
+
}
468
+
rows, err := s.db.QueryContext(ctx, `
469
+
SELECT r.at_uri, r.did, r.rkey, r.name, r.knot, r.description, r.topics,
470
+
r.website, r.source, r.spindle, r.repo_did, r.created_at, r.seen_at,
471
+
472
+
473
+
474
+
LEFT JOIN handles h ON h.did = r.did
475
+
WHERE r.created_at >= ?
476
+
AND (? = '' OR r.languages IS NOT NULL)
477
+
AND (? = 0 OR (r.source IS NOT NULL AND r.source != ''))
478
+
AND (? = 0 OR r.source IS NULL OR r.source = '')
479
+
ORDER BY r.created_at DESC
480
+
LIMIT ?
481
+
`, f.Since.UnixMilli(), f.Language, forksOnly, noForks, candidateLimit)
482
+
if err != nil {
483
+
return nil, err
484
+
}
485
+
486
+
487
+
488
+
489
+
490
+
if err != nil {
491
+
return nil, err
492
+
}
493
+
if f.Language != "" && r.Primary() != f.Language {
494
+
continue
495
+
}
496
+
out = append(out, r)
+45
-28
web/server.go
+45
-28
web/server.go
···
27
27
28
28
29
29
30
+
Language string
31
+
SinceRaw string // empty = no time filter
32
+
Threshold time.Time // zero = no time filter
33
+
Forks string // "" or "only"
34
+
}
30
35
36
+
func parseQuery(r *http.Request) query {
37
+
q := query{
38
+
Language: r.URL.Query().Get("language"),
39
+
SinceRaw: r.URL.Query().Get("since"),
40
+
Forks: r.URL.Query().Get("forks"),
41
+
}
42
+
if q.SinceRaw != "" {
43
+
if d, err := time.ParseDuration(q.SinceRaw); err == nil {
31
44
32
45
33
46
47
+
return q
48
+
}
34
49
50
+
func (q query) filter() store.ReposFilter {
51
+
return store.ReposFilter{
52
+
Language: q.Language,
53
+
Since: q.Threshold,
54
+
ForksOnly: q.Forks == "only",
55
+
NoForks: q.Forks == "no",
56
+
}
57
+
}
35
58
59
+
func reposJSON(s *store.Store) http.HandlerFunc {
60
+
return func(w http.ResponseWriter, r *http.Request) {
61
+
q := parseQuery(r)
62
+
repos, err := s.RecentRepos(r.Context(), q.filter(), 0)
63
+
if err != nil {
64
+
http.Error(w, err.Error(), http.StatusInternalServerError)
65
+
return
36
66
37
67
38
68
39
69
40
70
41
71
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
72
+
func reposHTML(s *store.Store) http.HandlerFunc {
73
+
return func(w http.ResponseWriter, r *http.Request) {
74
+
q := parseQuery(r)
75
+
repos, err := s.RecentRepos(r.Context(), q.filter(), 0)
76
+
if err != nil {
77
+
http.Error(w, err.Error(), http.StatusInternalServerError)
78
+
return
79
+
}
80
+
langs, err := s.PrimaryLanguages(r.Context())
81
+
if err != nil {
66
82
http.Error(w, err.Error(), http.StatusInternalServerError)
67
83
return
68
84
}
69
85
render(w, "repos", struct {
70
-
Active string
71
-
Query query
72
-
Repos []store.Repo
73
-
}{"repos", q, repos})
86
+
Active string
87
+
Query query
88
+
Repos []store.Repo
89
+
Languages []string
90
+
}{"repos", q, repos, langs})
74
91
}
75
92
}
76
93
+33
-1
web/templates/repos.html
+33
-1
web/templates/repos.html
···
1
1
{{define "repos"}}{{template "head" .}}
2
2
<form method="get">
3
-
<label>language<input name="language" value="{{.Query.Language}}" placeholder="any"></label>
3
+
<label>language
4
+
<select name="language">
5
+
<option value=""{{if eq .Query.Language ""}} selected{{end}}>any</option>
6
+
{{range .Languages}}
7
+
<option value="{{.}}"{{if eq $.Query.Language .}} selected{{end}}>{{.}}</option>
8
+
{{end}}
9
+
</select>
10
+
</label>
4
11
<label>since<input name="since" value="{{.Query.SinceRaw}}" placeholder="e.g. 24h"></label>
12
+
<label>forks
13
+
<select name="forks">
14
+
<option value=""{{if eq .Query.Forks ""}} selected{{end}}>any</option>
15
+
<option value="only"{{if eq .Query.Forks "only"}} selected{{end}}>only forks</option>
16
+
<option value="no"{{if eq .Query.Forks "no"}} selected{{end}}>no forks</option>
17
+
</select>
18
+
</label>
5
19
<button>filter</button>
6
20
</form>
21
+
<p class="summary">{{len .Repos}} repos · <a href="/repos.json?language={{.Query.Language}}&since={{.Query.SinceRaw}}&forks={{.Query.Forks}}">json</a></p>
22
+
{{if .Repos}}
23
+
<table>
24
+
<thead><tr><th>created</th><th>repo</th><th>knot</th><th>language</th><th>★</th><th>description</th></tr></thead>
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
{{- else -}}
33
+
{{.DID}}/{{.Name}}
34
+
{{- end -}}
35
+
{{if .Source}} <span class="fork" title="forked from {{.Source}}">↗</span>{{end}}
36
+
</td>
37
+
<td class="knot">{{.Knot}}</td>
38
+
<td class="lang">{{.Primary}}</td>
+1
web/templates/layout.html
+1
web/templates/layout.html
···
22
22
td.handle { font-weight: 600; }
23
23
td.handle a, td.knot a, .link a { color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; }
24
24
td.handle a:hover, td.knot a:hover, .link a:hover { border-bottom-style: solid; }
25
+
.fork { color: #888; cursor: help; margin-left: .25em; }
25
26
td.knot, td.did { color: #888; }
26
27
td.lang { color: #6a6; }
27
28
td.num { text-align: right; font-variant-numeric: tabular-nums; color: #888; }
History
3 rounds
0 comments
4 commits
expand
collapse
feat: filter by language
feat(web): additional filters
filter by forks/no forks/any
make language a dropdown (bad ux, but less typing)
feat(web): live stats
fix(ci): install make
merge conflicts detected
expand
collapse
expand
collapse
- store/store.go:10
- web/server.go:66
- web/templates/repos.html:1