1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "context"
7 "net/http"
8 "sort"
9 "strings"
10 "time"
11
12 "connectrpc.com/connect"
13 "github.com/a-h/templ"
14
15 pb "bnema.dev/ermite-api/gen/ermite"
16)
17
18// maxRepos bounds the repositories one page lists.
19const maxRepos = 1000
20
21// pageSize is the page size asked from the list RPCs: the largest the API
22// accepts.
23const pageSize = 100
24
25// listRepos reads the repositories the visitor may see, ordered by name.
26func (s *Server) listRepos(ctx context.Context) ([]*pb.Repo, error) {
27 var repos []*pb.Repo
28 token := ""
29 for len(repos) < maxRepos {
30 res, err := s.cfg.Repos.ListRepos(ctx, connect.NewRequest(&pb.ListReposRequest{PageSize: pageSize, PageToken: token}))
31 if err != nil {
32 return nil, err
33 }
34 repos = append(repos, res.Msg.GetRepos()...)
35 if token = res.Msg.GetNextPageToken(); token == "" {
36 break
37 }
38 }
39 return repos, nil
40}
41
42// homeCols are the sortable columns of the home list, in order.
43var homeCols = []struct{ key, label, class string }{
44 {"name", "name", "c-name"},
45 {"desc", "description", "c-desc"},
46 {"push", "last push", "c-when"},
47 {"branch", "branch", "c-ref"},
48}
49
50// sortRepos orders repos by the column key; ties keep the order by name.
51func sortRepos(repos []*pb.Repo, key string, desc bool) {
52 less := func(a, b *pb.Repo) bool {
53 switch key {
54 case "desc":
55 return strings.ToLower(a.GetDescription()) < strings.ToLower(b.GetDescription())
56 case "branch":
57 return a.GetDefaultBranch() < b.GetDefaultBranch()
58 case "push":
59 return a.GetPushedAt().AsTime().Before(b.GetPushedAt().AsTime())
60 }
61 return a.GetName() < b.GetName()
62 }
63 sort.SliceStable(repos, func(i, j int) bool {
64 if desc {
65 return less(repos[j], repos[i])
66 }
67 return less(repos[i], repos[j])
68 })
69}
70
71// home lists the repositories: /?sort=<col>&dir=asc|desc, newest push first
72// by default.
73func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) {
74 repos, err := s.listRepos(ctx)
75 if err != nil {
76 return nil, 0, err
77 }
78 key, desc := "push", true
79 for _, c := range homeCols {
80 if r.URL.Query().Get("sort") == c.key {
81 key, desc = c.key, c.key == "push"
82 }
83 }
84 if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" {
85 desc = d == "desc"
86 }
87 sortRepos(repos, key, desc)
88
89 now := s.cfg.Now()
90 p := HomePage{}
91 for _, c := range homeCols {
92 sc := SortCol{Label: c.label, Class: c.class, Dir: "none"}
93 next := "asc"
94 if c.key == key {
95 sc.Dir, sc.Mark = "ascending", "▲"
96 if desc {
97 sc.Dir, sc.Mark = "descending", "▼"
98 } else {
99 next = "desc"
100 }
101 }
102 sc.Href = "/?sort=" + c.key + "&dir=" + next
103 p.Cols = append(p.Cols, sc)
104 }
105 for i, repo := range repos {
106 p.Rows = append(p.Rows, homeRow(repo, now, i == 0))
107 }
108
109 l := s.base()
110 l.Title = s.instance
111 l.Live, l.LiveURL = true, liveURL("home", "")
112 l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...)
113 return Home(l, p), http.StatusOK, nil
114}
115
116// homeRow is the row of a repository in the home list.
117func homeRow(r *pb.Repo, now time.Time, cursor bool) HomeRow {
118 row := HomeRow{
119 ID: "repo-" + r.GetName(), Name: r.GetName(), Href: repoHref(r.GetName()),
120 Desc: r.GetDescription(), Pushed: "never", Branch: r.GetDefaultBranch(), Cursor: cursor,
121 }
122 if r.GetPushedAt().IsValid() && r.GetPushedAt().GetSeconds() != 0 {
123 t := r.GetPushedAt().AsTime()
124 row.Pushed, row.PushedAt = ago(now, t), rfc3339(t)
125 }
126 return row
127}
128
129// repoCmds are the repositories as targets of the ":" command line, but the
130// one being viewed.
131func repoCmds(repos []*pb.Repo, except string) []CmdItem {
132 var out []CmdItem
133 for _, r := range repos {
134 if r.GetName() != except {
135 out = append(out, CmdItem{Kind: "repo", Label: r.GetName(), Href: repoHref(r.GetName())})
136 }
137 }
138 return out
139}