// SPDX-License-Identifier: AGPL-3.0-or-later package web import ( "context" "net/http" "sort" "strings" "time" "connectrpc.com/connect" "github.com/a-h/templ" pb "bnema.dev/ermite-api/gen/ermite" ) // maxRepos bounds the repositories one page lists. const maxRepos = 1000 // pageSize is the page size asked from the list RPCs: the largest the API // accepts. const pageSize = 100 // listRepos reads the repositories the visitor may see, ordered by name. func (s *Server) listRepos(ctx context.Context) ([]*pb.Repo, error) { var repos []*pb.Repo token := "" for len(repos) < maxRepos { res, err := s.cfg.Repos.ListRepos(ctx, connect.NewRequest(&pb.ListReposRequest{PageSize: pageSize, PageToken: token})) if err != nil { return nil, err } repos = append(repos, res.Msg.GetRepos()...) if token = res.Msg.GetNextPageToken(); token == "" { break } } return repos, nil } // homeCols are the sortable columns of the home list, in order. var homeCols = []struct{ key, label, class string }{ {"name", "name", "c-name"}, {"desc", "description", "c-desc"}, {"push", "last push", "c-when"}, {"branch", "branch", "c-ref"}, } // sortRepos orders repos by the column key; ties keep the order by name. func sortRepos(repos []*pb.Repo, key string, desc bool) { less := func(a, b *pb.Repo) bool { switch key { case "desc": return strings.ToLower(a.GetDescription()) < strings.ToLower(b.GetDescription()) case "branch": return a.GetDefaultBranch() < b.GetDefaultBranch() case "push": return a.GetPushedAt().AsTime().Before(b.GetPushedAt().AsTime()) } return a.GetName() < b.GetName() } sort.SliceStable(repos, func(i, j int) bool { if desc { return less(repos[j], repos[i]) } return less(repos[i], repos[j]) }) } // home lists the repositories: /?sort=&dir=asc|desc, newest push first // by default. func (s *Server) home(ctx context.Context, r *http.Request) (templ.Component, int, error) { repos, err := s.listRepos(ctx) if err != nil { return nil, 0, err } key, desc := "push", true for _, c := range homeCols { if r.URL.Query().Get("sort") == c.key { key, desc = c.key, c.key == "push" } } if d := r.URL.Query().Get("dir"); d == "asc" || d == "desc" { desc = d == "desc" } sortRepos(repos, key, desc) now := s.cfg.Now() p := HomePage{} for _, c := range homeCols { sc := SortCol{Label: c.label, Class: c.class, Dir: "none"} next := "asc" if c.key == key { sc.Dir, sc.Mark = "ascending", "▲" if desc { sc.Dir, sc.Mark = "descending", "▼" } else { next = "desc" } } sc.Href = "/?sort=" + c.key + "&dir=" + next p.Cols = append(p.Cols, sc) } for i, repo := range repos { p.Rows = append(p.Rows, homeRow(repo, now, i == 0)) } l := s.base() l.Title = s.instance l.Live, l.LiveURL = true, liveURL("home", "") l.Cmd = append([]CmdItem{{Kind: "go", Label: "home", Href: "/"}}, repoCmds(repos, "")...) return Home(l, p), http.StatusOK, nil } // homeRow is the row of a repository in the home list. func homeRow(r *pb.Repo, now time.Time, cursor bool) HomeRow { row := HomeRow{ ID: "repo-" + r.GetName(), Name: r.GetName(), Href: repoHref(r.GetName()), Desc: r.GetDescription(), Pushed: "never", Branch: r.GetDefaultBranch(), Cursor: cursor, } if r.GetPushedAt().IsValid() && r.GetPushedAt().GetSeconds() != 0 { t := r.GetPushedAt().AsTime() row.Pushed, row.PushedAt = ago(now, t), rfc3339(t) } return row } // repoCmds are the repositories as targets of the ":" command line, but the // one being viewed. func repoCmds(repos []*pb.Repo, except string) []CmdItem { var out []CmdItem for _, r := range repos { if r.GetName() != except { out = append(out, CmdItem{Kind: "repo", Label: r.GetName(), Href: repoHref(r.GetName())}) } } return out }