v0.21.0 ef38ad0

home.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close