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 "github.com/a-h/templ"
13
14 "bnema.dev/ermite/internal/ports"
15)
16
17// maxRepos bounds the repositories one page lists.
18const maxRepos = 1000
19
20// pageSize is the page size asked from the commit lists: the largest the use
21// case accepts for them.
22const pageSize = 100
23
24// listRepos reads the repositories the visitor may see, ordered by name.
25// Every page of ListRepos scans the whole repository list and reads the HEAD
26// of each repository of the page, so the list is asked in one page of up to
27// maxRepos: one scan in the common case, whatever the cap of the use case is
28// (a lower one only means more pages, followed by their token).
29func (s *Server) listRepos(ctx context.Context) ([]ports.RepoView, error) {
30 var repos []ports.RepoView
31 token := ""
32 for len(repos) < maxRepos {
33 page, err := s.cfg.Repos.ListPage(ctx, principal(ctx), maxRepos, token)
34 if err != nil {
35 return nil, err
36 }
37 for _, repo := range page.Repos {
38 view, err := s.cfg.Repos.View(ctx, repo)
39 if err != nil {
40 return nil, err
41 }
42 repos = append(repos, view)
43 }
44 if token = page.NextPageToken; token == "" {
45 break
46 }
47 }
48 return repos, nil
49}
50
51// homeCols are the sortable columns of the home list, in order.
52var homeCols = []struct{ key, label, class string }{
53 {"name", "name", "c-name"},
54 {"desc", "description", "c-desc"},
55 {"push", "last push", "c-when"},
56 {"branch", "branch", "c-ref"},
57}
58
59// sortRepos orders repos by the column key; ties keep the order by name.
60func sortRepos(repos []ports.RepoView, key string, desc bool) {
61 less := func(a, b ports.RepoView) bool {
62 switch key {
63 case "desc":
64 return strings.ToLower(a.Description) < strings.ToLower(b.Description)
65 case "branch":
66 return a.DefaultBranch < b.DefaultBranch
67 case "push":
68 return a.PushedAt.Before(b.PushedAt)
69 }
70 return a.Name.String() < b.Name.String()
71 }
72 sort.SliceStable(repos, func(i, j int) bool {
73 if desc {
74 return less(repos[j], repos[i])
75 }
76 return less(repos[i], repos[j])
77 })
78}
79
80// home lists the repositories: /?sort=<col>&dir=asc|desc, newest push first
81// by default.
82func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) {
83 repos, err := s.listRepos(ctx)
84 if err != nil {
85 return nil, 0, err
86 }
87 key, desc := "push", true
88 for _, c := range homeCols {
89 if r.URL.Query().Get("sort") == c.key {
90 key, desc = c.key, c.key == "push"
91 }
92 }
93 if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" {
94 desc = d == "desc"
95 }
96 sortRepos(repos, key, desc)
97
98 now := s.cfg.Now()
99 p := HomePage{}
100 for _, c := range homeCols {
101 sc := SortCol{Label: c.label, Class: c.class, Dir: "none"}
102 next := "asc"
103 if c.key == key {
104 sc.Dir, sc.Mark = "ascending", "▲"
105 if desc {
106 sc.Dir, sc.Mark = "descending", "▼"
107 } else {
108 next = "desc"
109 }
110 }
111 sc.Href = "/?sort=" + c.key + "&dir=" + next
112 p.Cols = append(p.Cols, sc)
113 }
114 for i, repo := range repos {
115 p.Rows = append(p.Rows, homeRow(repo, now, i == 0))
116 }
117
118 l := s.base()
119 l.Title = s.instance
120 l.Live, l.LiveURL = true, liveURL("home", "")
121 l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...)
122 return Home(l, p), http.StatusOK, nil
123}
124
125// homeRow is the row of a repository in the home list.
126func homeRow(r ports.RepoView, now time.Time, cursor bool) HomeRow {
127 name := r.Name.String()
128 row := HomeRow{
129 ID: "repo-" + name, Name: name, Href: repoHref(name),
130 Desc: r.Description, Pushed: "never", Branch: r.DefaultBranch, Cursor: cursor,
131 }
132 if !r.PushedAt.IsZero() {
133 row.Pushed, row.PushedAt = ago(now, r.PushedAt), rfc3339(r.PushedAt)
134 }
135 return row
136}
137
138// repoCmds are the repositories as targets of the ":" command line, but the
139// one being viewed.
140func repoCmds(repos []ports.RepoView, except string) []CmdItem {
141 var out []CmdItem
142 for _, r := range repos {
143 if name := r.Name.String(); name != except {
144 out = append(out, CmdItem{Kind: "repo", Label: name, Href: repoHref(name)})
145 }
146 }
147 return out
148}